feat: indexer for cross level messages on Arbitrum (#9312)
* Initial version of x-level messages indexer * fixes for cspell and credo * new state of x-level messages * Monitoring of new L1-to-L2 messages on L1 * new batches discovery * fetcher workers in separate modules * proper name * Fix for responses without "id", e.g. "Too Many Requests" * update DB with new batches and corresponding data * update DB with confirmed blocks * fixes for cspell and credo * tracking commitments confirmations for L1 to L2 messages * Proper usign of max function * tracking completion of L2 to L1 messages * catchup historical messages to L2 * incorrect version of committed file * catchup historical messages from L2 and completion of L1-to-L2 messages * historical batches catchup * status for historical l2-to-l1 messages * address matching issue * catchup historical executions of L2-to-L1 messages * db query to find unconfirmed blocks gaps * first changes to catchup historical confirmations * finalized catchup of historical confirmations * 4844 blobs support * fix for the issue with multiple confirmations * limit amount of batches to handle at once * Use latest L1 block by fetchers if start block is not configured * merge issue fix * missed file * historical messages discovery * reduce logs severity * first iteration to improve documentation for new functionality * second iteration to improve documentation for new functionality * third iteration to improve documentation for new functionality * fourth iteration to improve documentation for new functionality * fifth iteration to improve documentation for new functionality * final iteration to improve documentation for new functionality * merge issues addressed * code review issues addressed * code review issues addressed * fix merge issue * raising exception in the case of DB inconsistency * fix formatting issue * termination case for RollupMessagesCatchup * code review comments addressed * code review comments addressed * consistency in primary keys * dialyzer fix * code review comments addressed * missed doc comment * code review comments addressed * updated indices creation as per code review comments * fix merge issue * configuration of intervals as time variables * TODO added to reflect improvement ability * database fields refactoring * association renaming * feat: APIv2 endpoints for Arbitrum messages and batches (#9963) * Arbitrum related info in Transaction and Block views * Views to get info about batches and messages * usage of committed for batches instead of confirmed * merge issues addressed * changes after merge * formatting issue fix * code review comment addressed * associations and fields in api response renamed * format issue addressed * feat: Arbitrum-specific fields in the block and transaction API endpoints (#10067) * Arbitrum related info in Transaction and Block views * Views to get info about batches and messages * usage of committed for batches instead of confirmed * merge issues addressed * changes after merge * formatting issue fix * block and transaction views extended * code review comment addressed * associations and fields in api response renamed * format issue addressed * fix credo issue * fix tests issues * ethereumjsonrpc test fail investigation * test issues fixespull/10129/head
parent
bf3f32137d
commit
35c885def5
@ -0,0 +1,163 @@ |
||||
defmodule BlockScoutWeb.API.V2.ArbitrumController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
import BlockScoutWeb.Chain, |
||||
only: [ |
||||
next_page_params: 4, |
||||
paging_options: 1, |
||||
split_list_by_page: 1 |
||||
] |
||||
|
||||
alias Explorer.PagingOptions |
||||
alias Explorer.Chain.Arbitrum.{L1Batch, Message, Reader} |
||||
|
||||
action_fallback(BlockScoutWeb.API.V2.FallbackController) |
||||
|
||||
@batch_necessity_by_association %{:commitment_transaction => :optional} |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/arbitrum/messages/:direction` endpoint. |
||||
""" |
||||
@spec messages(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def messages(conn, %{"direction" => direction} = params) do |
||||
options = |
||||
params |
||||
|> paging_options() |
||||
|> Keyword.put(:api?, true) |
||||
|
||||
{messages, next_page} = |
||||
direction |
||||
|> Reader.messages(options) |
||||
|> split_list_by_page() |
||||
|
||||
next_page_params = |
||||
next_page_params( |
||||
next_page, |
||||
messages, |
||||
params, |
||||
fn %Message{message_id: msg_id} -> %{"id" => msg_id} end |
||||
) |
||||
|
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_messages, %{ |
||||
messages: messages, |
||||
next_page_params: next_page_params |
||||
}) |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/arbitrum/messages/:direction/count` endpoint. |
||||
""" |
||||
@spec messages_count(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def messages_count(conn, %{"direction" => direction} = _params) do |
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_messages_count, %{count: Reader.messages_count(direction, api?: true)}) |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/arbitrum/batches/:batch_number` endpoint. |
||||
""" |
||||
@spec batch(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def batch(conn, %{"batch_number" => batch_number} = _params) do |
||||
case Reader.batch( |
||||
batch_number, |
||||
necessity_by_association: @batch_necessity_by_association, |
||||
api?: true |
||||
) do |
||||
{:ok, batch} -> |
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_batch, %{batch: batch}) |
||||
|
||||
{:error, :not_found} = res -> |
||||
res |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/arbitrum/batches/count` endpoint. |
||||
""" |
||||
@spec batches_count(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def batches_count(conn, _params) do |
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_batches_count, %{count: Reader.batches_count(api?: true)}) |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/arbitrum/batches` endpoint. |
||||
""" |
||||
@spec batches(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def batches(conn, params) do |
||||
{batches, next_page} = |
||||
params |
||||
|> paging_options() |
||||
|> Keyword.put(:necessity_by_association, @batch_necessity_by_association) |
||||
|> Keyword.put(:api?, true) |
||||
|> Reader.batches() |
||||
|> split_list_by_page() |
||||
|
||||
next_page_params = |
||||
next_page_params( |
||||
next_page, |
||||
batches, |
||||
params, |
||||
fn %L1Batch{number: number} -> %{"number" => number} end |
||||
) |
||||
|
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_batches, %{ |
||||
batches: batches, |
||||
next_page_params: next_page_params |
||||
}) |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/main-page/arbitrum/batches/committed` endpoint. |
||||
""" |
||||
@spec batches_committed(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def batches_committed(conn, _params) do |
||||
batches = |
||||
[] |
||||
|> Keyword.put(:necessity_by_association, @batch_necessity_by_association) |
||||
|> Keyword.put(:api?, true) |
||||
|> Keyword.put(:committed?, true) |
||||
|> Reader.batches() |
||||
|
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_batches, %{batches: batches}) |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/main-page/arbitrum/batches/latest-number` endpoint. |
||||
""" |
||||
@spec batch_latest_number(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def batch_latest_number(conn, _params) do |
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_batch_latest_number, %{number: batch_latest_number()}) |
||||
end |
||||
|
||||
defp batch_latest_number do |
||||
case Reader.batch(:latest, api?: true) do |
||||
{:ok, batch} -> batch.number |
||||
{:error, :not_found} -> 0 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/main-page/arbitrum/messages/to-rollup` endpoint. |
||||
""" |
||||
@spec recent_messages_to_l2(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def recent_messages_to_l2(conn, _params) do |
||||
messages = Reader.relayed_l1_to_l2_messages(paging_options: %PagingOptions{page_size: 6}, api?: true) |
||||
|
||||
conn |
||||
|> put_status(200) |
||||
|> render(:arbitrum_messages, %{messages: messages}) |
||||
end |
||||
end |
@ -0,0 +1,425 @@ |
||||
defmodule BlockScoutWeb.API.V2.ArbitrumView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper |
||||
alias Explorer.Chain.{Block, Hash, Transaction, Wei} |
||||
alias Explorer.Chain.Arbitrum.{L1Batch, LifecycleTransaction} |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/arbitrum/messages/:direction` endpoint. |
||||
""" |
||||
@spec render(binary(), map()) :: map() | non_neg_integer() |
||||
def render("arbitrum_messages.json", %{ |
||||
messages: messages, |
||||
next_page_params: next_page_params |
||||
}) do |
||||
messages_out = |
||||
messages |
||||
|> Enum.map(fn msg -> |
||||
%{ |
||||
"id" => msg.message_id, |
||||
"origination_address" => msg.originator_address, |
||||
"origination_transaction_hash" => msg.originating_transaction_hash, |
||||
"origination_timestamp" => msg.origination_timestamp, |
||||
"origination_transaction_block_number" => msg.originating_transaction_block_number, |
||||
"completion_transaction_hash" => msg.completion_transaction_hash, |
||||
"status" => msg.status |
||||
} |
||||
end) |
||||
|
||||
%{ |
||||
items: messages_out, |
||||
next_page_params: next_page_params |
||||
} |
||||
end |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/main-page/arbitrum/messages/to-rollup` endpoint. |
||||
""" |
||||
def render("arbitrum_messages.json", %{messages: messages}) do |
||||
messages_out = |
||||
messages |
||||
|> Enum.map(fn msg -> |
||||
%{ |
||||
"origination_transaction_hash" => msg.originating_transaction_hash, |
||||
"origination_timestamp" => msg.origination_timestamp, |
||||
"origination_transaction_block_number" => msg.originating_transaction_block_number, |
||||
"completion_transaction_hash" => msg.completion_transaction_hash |
||||
} |
||||
end) |
||||
|
||||
%{items: messages_out} |
||||
end |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/arbitrum/messages/:direction/count` endpoint. |
||||
""" |
||||
def render("arbitrum_messages_count.json", %{count: count}) do |
||||
count |
||||
end |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/arbitrum/batches/:batch_number` endpoint. |
||||
""" |
||||
def render("arbitrum_batch.json", %{batch: batch}) do |
||||
%{ |
||||
"number" => batch.number, |
||||
"transactions_count" => batch.transactions_count, |
||||
"start_block" => batch.start_block, |
||||
"end_block" => batch.end_block, |
||||
"before_acc" => batch.before_acc, |
||||
"after_acc" => batch.after_acc |
||||
} |
||||
|> add_l1_tx_info(batch) |
||||
end |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/arbitrum/batches` endpoint. |
||||
""" |
||||
def render("arbitrum_batches.json", %{ |
||||
batches: batches, |
||||
next_page_params: next_page_params |
||||
}) do |
||||
%{ |
||||
items: render_arbitrum_batches(batches), |
||||
next_page_params: next_page_params |
||||
} |
||||
end |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/main-page/arbitrum/batches/committed` endpoint. |
||||
""" |
||||
def render("arbitrum_batches.json", %{batches: batches}) do |
||||
%{items: render_arbitrum_batches(batches)} |
||||
end |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/arbitrum/batches/count` endpoint. |
||||
""" |
||||
def render("arbitrum_batches_count.json", %{count: count}) do |
||||
count |
||||
end |
||||
|
||||
@doc """ |
||||
Function to render GET requests to `/api/v2/main-page/arbitrum/batches/latest-number` endpoint. |
||||
""" |
||||
def render("arbitrum_batch_latest_number.json", %{number: number}) do |
||||
number |
||||
end |
||||
|
||||
# Transforms a list of L1 batches into a map format for HTTP response. |
||||
# |
||||
# This function processes a list of Arbitrum L1 batches and converts each batch into |
||||
# a map that includes basic batch information and details of the associated |
||||
# transaction that committed the batch to L1. |
||||
# |
||||
# ## Parameters |
||||
# - `batches`: A list of `Explorer.Chain.Arbitrum.L1Batch` entries. |
||||
# |
||||
# ## Returns |
||||
# - A list of maps with detailed information about each batch, formatted for use |
||||
# in JSON HTTP responses. |
||||
@spec render_arbitrum_batches([L1Batch]) :: [map()] |
||||
defp render_arbitrum_batches(batches) do |
||||
Enum.map(batches, fn batch -> |
||||
%{ |
||||
"number" => batch.number, |
||||
"transactions_count" => batch.transactions_count, |
||||
"block_count" => batch.end_block - batch.start_block + 1 |
||||
} |
||||
|> add_l1_tx_info(batch) |
||||
end) |
||||
end |
||||
|
||||
@doc """ |
||||
Extends the json output with a sub-map containing information related Arbitrum. |
||||
|
||||
## Parameters |
||||
- `out_json`: a map defining output json which will be extended |
||||
- `transaction`: transaction structure containing Arbitrum related data |
||||
|
||||
## Returns |
||||
A map extended with data related Arbitrum rollup |
||||
""" |
||||
@spec extend_transaction_json_response(map(), %{ |
||||
:__struct__ => Transaction, |
||||
:arbitrum_batch => any(), |
||||
:arbitrum_commitment_transaction => any(), |
||||
:arbitrum_confirmation_transaction => any(), |
||||
:arbitrum_message_to_l2 => any(), |
||||
:arbitrum_message_from_l2 => any(), |
||||
:gas_used_for_l1 => Decimal.t(), |
||||
:gas_used => Decimal.t(), |
||||
:gas_price => Wei.t(), |
||||
optional(any()) => any() |
||||
}) :: map() |
||||
def extend_transaction_json_response(out_json, %Transaction{} = transaction) do |
||||
arbitrum_info = |
||||
%{} |
||||
|> extend_with_settlement_info(transaction) |
||||
|> extend_if_message(transaction) |
||||
|> extend_with_transaction_info(transaction) |
||||
|
||||
Map.put(out_json, "arbitrum", arbitrum_info) |
||||
end |
||||
|
||||
@doc """ |
||||
Extends the json output with a sub-map containing information related Arbitrum. |
||||
|
||||
## Parameters |
||||
- `out_json`: a map defining output json which will be extended |
||||
- `block`: block structure containing Arbitrum related data |
||||
|
||||
## Returns |
||||
A map extended with data related Arbitrum rollup |
||||
""" |
||||
@spec extend_block_json_response(map(), %{ |
||||
:__struct__ => Block, |
||||
:arbitrum_batch => any(), |
||||
:arbitrum_commitment_transaction => any(), |
||||
:arbitrum_confirmation_transaction => any(), |
||||
:nonce => Hash.Nonce.t(), |
||||
:send_count => non_neg_integer(), |
||||
:send_root => Hash.Full.t(), |
||||
:l1_block_number => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}) :: map() |
||||
def extend_block_json_response(out_json, %Block{} = block) do |
||||
arbitrum_info = |
||||
%{} |
||||
|> extend_with_settlement_info(block) |
||||
|> extend_with_block_info(block) |
||||
|
||||
Map.put(out_json, "arbitrum", arbitrum_info) |
||||
end |
||||
|
||||
# Augments an output JSON with settlement-related information such as batch number and L1 transaction details to JSON. |
||||
@spec extend_with_settlement_info(map(), %{ |
||||
:__struct__ => Block | Transaction, |
||||
:arbitrum_batch => any(), |
||||
:arbitrum_commitment_transaction => any(), |
||||
:arbitrum_confirmation_transaction => any(), |
||||
optional(any()) => any() |
||||
}) :: map() |
||||
defp extend_with_settlement_info(out_json, arbitrum_entity) do |
||||
out_json |
||||
|> add_l1_txs_info_and_status(%{ |
||||
batch_number: get_batch_number(arbitrum_entity), |
||||
commitment_transaction: arbitrum_entity.arbitrum_commitment_transaction, |
||||
confirmation_transaction: arbitrum_entity.arbitrum_confirmation_transaction |
||||
}) |
||||
|> Map.put("batch_number", get_batch_number(arbitrum_entity)) |
||||
end |
||||
|
||||
# Retrieves the batch number from an Arbitrum block or transaction if the batch |
||||
# data is loaded. |
||||
@spec get_batch_number(%{ |
||||
:__struct__ => Block | Transaction, |
||||
:arbitrum_batch => any(), |
||||
optional(any()) => any() |
||||
}) :: nil | non_neg_integer() |
||||
defp get_batch_number(arbitrum_entity) do |
||||
case Map.get(arbitrum_entity, :arbitrum_batch) do |
||||
nil -> nil |
||||
%Ecto.Association.NotLoaded{} -> nil |
||||
value -> value.number |
||||
end |
||||
end |
||||
|
||||
# Augments an output JSON with commit transaction details and its status. |
||||
@spec add_l1_tx_info(map(), %{ |
||||
:__struct__ => L1Batch, |
||||
:commitment_transaction => any(), |
||||
optional(any()) => any() |
||||
}) :: map() |
||||
defp add_l1_tx_info(out_json, %L1Batch{} = batch) do |
||||
l1_tx = %{commitment_transaction: handle_associated_l1_txs_properly(batch.commitment_transaction)} |
||||
|
||||
out_json |
||||
|> Map.merge(%{ |
||||
"commitment_transaction" => %{ |
||||
"hash" => APIV2Helper.get_2map_data(l1_tx, :commitment_transaction, :hash), |
||||
"block_number" => APIV2Helper.get_2map_data(l1_tx, :commitment_transaction, :block), |
||||
"timestamp" => APIV2Helper.get_2map_data(l1_tx, :commitment_transaction, :ts), |
||||
"status" => APIV2Helper.get_2map_data(l1_tx, :commitment_transaction, :status) |
||||
} |
||||
}) |
||||
end |
||||
|
||||
# Augments an output JSON with commit and confirm transaction details and their statuses. |
||||
@spec add_l1_txs_info_and_status(map(), %{ |
||||
:commitment_transaction => any(), |
||||
:confirmation_transaction => any(), |
||||
optional(:batch_number) => any() |
||||
}) :: map() |
||||
defp add_l1_txs_info_and_status(out_json, arbitrum_item) |
||||
when is_map(arbitrum_item) and |
||||
is_map_key(arbitrum_item, :commitment_transaction) and |
||||
is_map_key(arbitrum_item, :confirmation_transaction) do |
||||
l1_txs = get_associated_l1_txs(arbitrum_item) |
||||
|
||||
out_json |
||||
|> Map.merge(%{ |
||||
"status" => block_or_transaction_status(arbitrum_item), |
||||
"commitment_transaction" => %{ |
||||
"hash" => APIV2Helper.get_2map_data(l1_txs, :commitment_transaction, :hash), |
||||
"timestamp" => APIV2Helper.get_2map_data(l1_txs, :commitment_transaction, :ts), |
||||
"status" => APIV2Helper.get_2map_data(l1_txs, :commitment_transaction, :status) |
||||
}, |
||||
"confirmation_transaction" => %{ |
||||
"hash" => APIV2Helper.get_2map_data(l1_txs, :confirmation_transaction, :hash), |
||||
"timestamp" => APIV2Helper.get_2map_data(l1_txs, :confirmation_transaction, :ts), |
||||
"status" => APIV2Helper.get_2map_data(l1_txs, :confirmation_transaction, :status) |
||||
} |
||||
}) |
||||
end |
||||
|
||||
# Extract transaction hash and block number, timestamp, finalization status for |
||||
# L1 transactions associated with an Arbitrum rollup entity: transaction or block. |
||||
# |
||||
# ## Parameters |
||||
# - `arbitrum_item`: a short description of a transaction, or block. |
||||
# |
||||
# ## Returns |
||||
# A map containing nesting maps describing corresponding L1 transactions |
||||
@spec get_associated_l1_txs(%{ |
||||
:commitment_transaction => any(), |
||||
:confirmation_transaction => any(), |
||||
optional(any()) => any() |
||||
}) :: %{ |
||||
:commitment_transaction => |
||||
nil |
||||
| %{ |
||||
:hash => nil | binary(), |
||||
:block_number => nil | non_neg_integer(), |
||||
:ts => nil | DateTime.t(), |
||||
:status => nil | :finalized | :unfinalized |
||||
}, |
||||
:confirmation_transaction => |
||||
nil |
||||
| %{ |
||||
:hash => nil | binary(), |
||||
:block_number => nil | non_neg_integer(), |
||||
:ts => nil | DateTime.t(), |
||||
:status => nil | :finalized | :unfinalized |
||||
} |
||||
} |
||||
defp get_associated_l1_txs(arbitrum_item) do |
||||
[:commitment_transaction, :confirmation_transaction] |
||||
|> Enum.reduce(%{}, fn key, l1_txs -> |
||||
Map.put(l1_txs, key, handle_associated_l1_txs_properly(Map.get(arbitrum_item, key))) |
||||
end) |
||||
end |
||||
|
||||
# Returns details of an associated L1 transaction or nil if not loaded or not available. |
||||
@spec handle_associated_l1_txs_properly(LifecycleTransaction | Ecto.Association.NotLoaded.t() | nil) :: |
||||
nil |
||||
| %{ |
||||
:hash => nil | binary(), |
||||
:block => nil | non_neg_integer(), |
||||
:ts => nil | DateTime.t(), |
||||
:status => nil | :finalized | :unfinalized |
||||
} |
||||
defp handle_associated_l1_txs_properly(associated_l1_tx) do |
||||
case associated_l1_tx do |
||||
nil -> nil |
||||
%Ecto.Association.NotLoaded{} -> nil |
||||
value -> %{hash: value.hash, block: value.block_number, ts: value.timestamp, status: value.status} |
||||
end |
||||
end |
||||
|
||||
# Inspects L1 transactions of a rollup block or transaction to determine its status. |
||||
# |
||||
# ## Parameters |
||||
# - `arbitrum_item`: An Arbitrum transaction or block. |
||||
# |
||||
# ## Returns |
||||
# A string with one of predefined statuses |
||||
@spec block_or_transaction_status(%{ |
||||
:commitment_transaction => any(), |
||||
:confirmation_transaction => any(), |
||||
optional(:batch_number) => any() |
||||
}) :: String.t() |
||||
defp block_or_transaction_status(arbitrum_item) do |
||||
cond do |
||||
APIV2Helper.specified?(arbitrum_item.confirmation_transaction) -> "Confirmed on base" |
||||
APIV2Helper.specified?(arbitrum_item.commitment_transaction) -> "Sent to base" |
||||
not is_nil(arbitrum_item.batch_number) -> "Sealed on rollup" |
||||
true -> "Processed on rollup" |
||||
end |
||||
end |
||||
|
||||
# Determines if an Arbitrum transaction contains a cross-chain message and extends |
||||
# the incoming map with the `contains_message` field to reflect the direction of |
||||
# the message. |
||||
# |
||||
# ## Parameters |
||||
# - `arbitrum_tx`: An Arbitrum transaction. |
||||
# |
||||
# ## Returns |
||||
# - A map extended with a field indicating the direction of the message. |
||||
@spec extend_if_message(map(), %{ |
||||
:__struct__ => Transaction, |
||||
:arbitrum_message_to_l2 => any(), |
||||
:arbitrum_message_from_l2 => any(), |
||||
optional(any()) => any() |
||||
}) :: map() |
||||
defp extend_if_message(arbitrum_json, %Transaction{} = arbitrum_tx) do |
||||
message_type = |
||||
case {APIV2Helper.specified?(arbitrum_tx.arbitrum_message_to_l2), |
||||
APIV2Helper.specified?(arbitrum_tx.arbitrum_message_from_l2)} do |
||||
{true, false} -> "incoming" |
||||
{false, true} -> "outcoming" |
||||
_ -> nil |
||||
end |
||||
|
||||
Map.put(arbitrum_json, "contains_message", message_type) |
||||
end |
||||
|
||||
# Extends the output JSON with information from Arbitrum-specific fields of the transaction. |
||||
@spec extend_with_transaction_info(map(), %{ |
||||
:__struct__ => Transaction, |
||||
:gas_used_for_l1 => Decimal.t(), |
||||
:gas_used => Decimal.t(), |
||||
:gas_price => Wei.t(), |
||||
optional(any()) => any() |
||||
}) :: map() |
||||
defp extend_with_transaction_info(out_json, %Transaction{} = arbitrum_tx) do |
||||
gas_used_for_l2 = |
||||
arbitrum_tx.gas_used |
||||
|> Decimal.sub(arbitrum_tx.gas_used_for_l1) |
||||
|
||||
poster_fee = |
||||
arbitrum_tx.gas_price |
||||
|> Wei.to(:wei) |
||||
|> Decimal.mult(arbitrum_tx.gas_used_for_l1) |
||||
|
||||
network_fee = |
||||
arbitrum_tx.gas_price |
||||
|> Wei.to(:wei) |
||||
|> Decimal.mult(gas_used_for_l2) |
||||
|
||||
out_json |
||||
|> Map.put("gas_used_for_l1", arbitrum_tx.gas_used_for_l1) |
||||
|> Map.put("gas_used_for_l2", gas_used_for_l2) |
||||
|> Map.put("poster_fee", poster_fee) |
||||
|> Map.put("network_fee", network_fee) |
||||
end |
||||
|
||||
# Extends the output JSON with information from the Arbitrum-specific fields of the block. |
||||
@spec extend_with_block_info(map(), %{ |
||||
:__struct__ => Block, |
||||
:nonce => Hash.Nonce.t(), |
||||
:send_count => non_neg_integer(), |
||||
:send_root => Hash.Full.t(), |
||||
:l1_block_number => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}) :: map() |
||||
defp extend_with_block_info(out_json, %Block{} = arbitrum_block) do |
||||
out_json |
||||
|> Map.put("delayed_messages", Hash.to_integer(arbitrum_block.nonce)) |
||||
|> Map.put("l1_block_height", arbitrum_block.l1_block_number) |
||||
|> Map.put("send_count", arbitrum_block.send_count) |
||||
|> Map.put("send_root", arbitrum_block.send_root) |
||||
end |
||||
end |
@ -0,0 +1,53 @@ |
||||
defmodule Explorer.Chain.Arbitrum.BatchBlock do |
||||
@moduledoc """ |
||||
Models a list of blocks related to a batch for Arbitrum. |
||||
|
||||
Changes in the schema should be reflected in the bulk import module: |
||||
- Explorer.Chain.Import.Runner.Arbitrum.BatchBlocks |
||||
|
||||
Migrations: |
||||
- Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.Arbitrum.{L1Batch, LifecycleTransaction} |
||||
|
||||
@optional_attrs ~w(confirmation_id)a |
||||
|
||||
@required_attrs ~w(batch_number block_number)a |
||||
|
||||
@type t :: %__MODULE__{ |
||||
batch_number: non_neg_integer(), |
||||
batch: %Ecto.Association.NotLoaded{} | L1Batch.t() | nil, |
||||
block_number: non_neg_integer(), |
||||
confirmation_id: non_neg_integer() | nil, |
||||
confirmation_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil |
||||
} |
||||
|
||||
@primary_key {:block_number, :integer, autogenerate: false} |
||||
schema "arbitrum_batch_l2_blocks" do |
||||
belongs_to(:batch, L1Batch, foreign_key: :batch_number, references: :number, type: :integer) |
||||
|
||||
belongs_to(:confirmation_transaction, LifecycleTransaction, |
||||
foreign_key: :confirmation_id, |
||||
references: :id, |
||||
type: :integer |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@doc """ |
||||
Validates that the `attrs` are valid. |
||||
""" |
||||
@spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() |
||||
def changeset(%__MODULE__{} = items, attrs \\ %{}) do |
||||
items |
||||
|> cast(attrs, @required_attrs ++ @optional_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:batch_number) |
||||
|> foreign_key_constraint(:confirmation_id) |
||||
|> unique_constraint(:block_number) |
||||
end |
||||
end |
@ -0,0 +1,52 @@ |
||||
defmodule Explorer.Chain.Arbitrum.BatchTransaction do |
||||
@moduledoc """ |
||||
Models a list of transactions related to a batch for Arbitrum. |
||||
|
||||
Changes in the schema should be reflected in the bulk import module: |
||||
- Explorer.Chain.Import.Runner.Arbitrum.BatchTransactions |
||||
|
||||
Migrations: |
||||
- Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.Arbitrum.L1Batch |
||||
alias Explorer.Chain.{Hash, Transaction} |
||||
|
||||
@required_attrs ~w(batch_number tx_hash)a |
||||
|
||||
@type t :: %__MODULE__{ |
||||
batch_number: non_neg_integer(), |
||||
batch: %Ecto.Association.NotLoaded{} | L1Batch.t() | nil, |
||||
tx_hash: Hash.t(), |
||||
l2_transaction: %Ecto.Association.NotLoaded{} | Transaction.t() | nil |
||||
} |
||||
|
||||
@primary_key false |
||||
schema "arbitrum_batch_l2_transactions" do |
||||
belongs_to(:batch, L1Batch, foreign_key: :batch_number, references: :number, type: :integer) |
||||
|
||||
belongs_to(:l2_transaction, Transaction, |
||||
foreign_key: :tx_hash, |
||||
primary_key: true, |
||||
references: :hash, |
||||
type: Hash.Full |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@doc """ |
||||
Validates that the `attrs` are valid. |
||||
""" |
||||
@spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() |
||||
def changeset(%__MODULE__{} = transactions, attrs \\ %{}) do |
||||
transactions |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:batch_number) |
||||
|> foreign_key_constraint(:block_hash) |
||||
|> unique_constraint(:tx_hash) |
||||
end |
||||
end |
@ -0,0 +1,62 @@ |
||||
defmodule Explorer.Chain.Arbitrum.L1Batch do |
||||
@moduledoc """ |
||||
Models an L1 batch for Arbitrum. |
||||
|
||||
Changes in the schema should be reflected in the bulk import module: |
||||
- Explorer.Chain.Import.Runner.Arbitrum.L1Batches |
||||
|
||||
Migrations: |
||||
- Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.{ |
||||
Block, |
||||
Hash |
||||
} |
||||
|
||||
alias Explorer.Chain.Arbitrum.LifecycleTransaction |
||||
|
||||
@required_attrs ~w(number transactions_count start_block end_block before_acc after_acc commitment_id)a |
||||
|
||||
@type t :: %__MODULE__{ |
||||
number: non_neg_integer(), |
||||
transactions_count: non_neg_integer(), |
||||
start_block: Block.block_number(), |
||||
end_block: Block.block_number(), |
||||
before_acc: Hash.t(), |
||||
after_acc: Hash.t(), |
||||
commitment_id: non_neg_integer(), |
||||
commitment_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil |
||||
} |
||||
|
||||
@primary_key {:number, :integer, autogenerate: false} |
||||
schema "arbitrum_l1_batches" do |
||||
field(:transactions_count, :integer) |
||||
field(:start_block, :integer) |
||||
field(:end_block, :integer) |
||||
field(:before_acc, Hash.Full) |
||||
field(:after_acc, Hash.Full) |
||||
|
||||
belongs_to(:commitment_transaction, LifecycleTransaction, |
||||
foreign_key: :commitment_id, |
||||
references: :id, |
||||
type: :integer |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@doc """ |
||||
Validates that the `attrs` are valid. |
||||
""" |
||||
@spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() |
||||
def changeset(%__MODULE__{} = batches, attrs \\ %{}) do |
||||
batches |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:commitment_id) |
||||
|> unique_constraint(:number) |
||||
end |
||||
end |
@ -0,0 +1,46 @@ |
||||
defmodule Explorer.Chain.Arbitrum.L1Execution do |
||||
@moduledoc """ |
||||
Models a list of execution transactions related to a L2 to L1 messages on Arbitrum. |
||||
|
||||
Changes in the schema should be reflected in the bulk import module: |
||||
- Explorer.Chain.Import.Runner.Arbitrum.L1Executions |
||||
|
||||
Migrations: |
||||
- Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.Arbitrum.LifecycleTransaction |
||||
|
||||
@required_attrs ~w(message_id execution_id)a |
||||
|
||||
@type t :: %__MODULE__{ |
||||
message_id: non_neg_integer(), |
||||
execution_id: non_neg_integer(), |
||||
execution_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil |
||||
} |
||||
|
||||
@primary_key {:message_id, :integer, autogenerate: false} |
||||
schema "arbitrum_l1_executions" do |
||||
belongs_to(:execution_transaction, LifecycleTransaction, |
||||
foreign_key: :execution_id, |
||||
references: :id, |
||||
type: :integer |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@doc """ |
||||
Validates that the `attrs` are valid. |
||||
""" |
||||
@spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() |
||||
def changeset(%__MODULE__{} = items, attrs \\ %{}) do |
||||
items |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:execution_id) |
||||
|> unique_constraint(:message_id) |
||||
end |
||||
end |
@ -0,0 +1,54 @@ |
||||
defmodule Explorer.Chain.Arbitrum.LifecycleTransaction do |
||||
@moduledoc """ |
||||
Models an L1 lifecycle transaction for Arbitrum. |
||||
|
||||
Changes in the schema should be reflected in the bulk import module: |
||||
- Explorer.Chain.Import.Runner.Arbitrum.LifecycleTransactions |
||||
|
||||
Migrations: |
||||
- Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.{ |
||||
Block, |
||||
Hash |
||||
} |
||||
|
||||
alias Explorer.Chain.Arbitrum.{BatchBlock, L1Batch} |
||||
|
||||
@required_attrs ~w(id hash block_number timestamp status)a |
||||
|
||||
@type t :: %__MODULE__{ |
||||
id: non_neg_integer(), |
||||
hash: Hash.t(), |
||||
block_number: Block.block_number(), |
||||
timestamp: DateTime.t(), |
||||
status: String.t() |
||||
} |
||||
|
||||
@primary_key {:id, :integer, autogenerate: false} |
||||
schema "arbitrum_lifecycle_l1_transactions" do |
||||
field(:hash, Hash.Full) |
||||
field(:block_number, :integer) |
||||
field(:timestamp, :utc_datetime_usec) |
||||
field(:status, Ecto.Enum, values: [:unfinalized, :finalized]) |
||||
|
||||
has_many(:committed_batches, L1Batch, foreign_key: :commitment_id) |
||||
has_many(:confirmed_blocks, BatchBlock, foreign_key: :confirmation_id) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@doc """ |
||||
Validates that the `attrs` are valid. |
||||
""" |
||||
@spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() |
||||
def changeset(%__MODULE__{} = txn, attrs \\ %{}) do |
||||
txn |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> unique_constraint([:id, :hash]) |
||||
end |
||||
end |
@ -0,0 +1,57 @@ |
||||
defmodule Explorer.Chain.Arbitrum.Message do |
||||
@moduledoc """ |
||||
Models an L1<->L2 messages on Arbitrum. |
||||
|
||||
Changes in the schema should be reflected in the bulk import module: |
||||
- Explorer.Chain.Import.Runner.Arbitrum.Messages |
||||
|
||||
Migrations: |
||||
- Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.{Block, Hash} |
||||
|
||||
@optional_attrs ~w(originator_address originating_transaction_hash origination_timestamp originating_transaction_block_number completion_transaction_hash)a |
||||
|
||||
@required_attrs ~w(direction message_id status)a |
||||
|
||||
@allowed_attrs @optional_attrs ++ @required_attrs |
||||
|
||||
@type t :: %__MODULE__{ |
||||
direction: String.t(), |
||||
message_id: non_neg_integer(), |
||||
originator_address: Hash.Address.t() | nil, |
||||
originating_transaction_hash: Hash.t() | nil, |
||||
origination_timestamp: DateTime.t() | nil, |
||||
originating_transaction_block_number: Block.block_number() | nil, |
||||
completion_transaction_hash: Hash.t() | nil, |
||||
status: String.t() |
||||
} |
||||
|
||||
@primary_key false |
||||
schema "arbitrum_crosslevel_messages" do |
||||
field(:direction, Ecto.Enum, values: [:to_l2, :from_l2], primary_key: true) |
||||
field(:message_id, :integer, primary_key: true) |
||||
field(:originator_address, Hash.Address) |
||||
field(:originating_transaction_hash, Hash.Full) |
||||
field(:origination_timestamp, :utc_datetime_usec) |
||||
field(:originating_transaction_block_number, :integer) |
||||
field(:completion_transaction_hash, Hash.Full) |
||||
field(:status, Ecto.Enum, values: [:initiated, :sent, :confirmed, :relayed]) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@doc """ |
||||
Validates that the `attrs` are valid. |
||||
""" |
||||
@spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() |
||||
def changeset(%__MODULE__{} = txn, attrs \\ %{}) do |
||||
txn |
||||
|> cast(attrs, @allowed_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> unique_constraint([:direction, :message_id]) |
||||
end |
||||
end |
@ -0,0 +1,913 @@ |
||||
defmodule Explorer.Chain.Arbitrum.Reader do |
||||
@moduledoc """ |
||||
Contains read functions for Arbitrum modules. |
||||
""" |
||||
|
||||
import Ecto.Query, only: [from: 2, limit: 2, order_by: 2, subquery: 1, where: 2, where: 3] |
||||
import Explorer.Chain, only: [select_repo: 1] |
||||
|
||||
alias Explorer.Chain.Arbitrum.{BatchBlock, BatchTransaction, L1Batch, L1Execution, LifecycleTransaction, Message} |
||||
|
||||
alias Explorer.{Chain, PagingOptions, Repo} |
||||
|
||||
alias Explorer.Chain.Block, as: FullBlock |
||||
alias Explorer.Chain.{Hash, Transaction} |
||||
|
||||
@doc """ |
||||
Retrieves the number of the latest L1 block where an L1-to-L2 message was discovered. |
||||
|
||||
## Returns |
||||
- The number of L1 block, or `nil` if no L1-to-L2 messages are found. |
||||
""" |
||||
@spec l1_block_of_latest_discovered_message_to_l2() :: FullBlock.block_number() | nil |
||||
def l1_block_of_latest_discovered_message_to_l2 do |
||||
query = |
||||
from(msg in Message, |
||||
select: msg.originating_transaction_block_number, |
||||
where: msg.direction == :to_l2 and not is_nil(msg.originating_transaction_block_number), |
||||
order_by: [desc: msg.message_id], |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the earliest L1 block where an L1-to-L2 message was discovered. |
||||
|
||||
## Returns |
||||
- The number of L1 block, or `nil` if no L1-to-L2 messages are found. |
||||
""" |
||||
@spec l1_block_of_earliest_discovered_message_to_l2() :: FullBlock.block_number() | nil |
||||
def l1_block_of_earliest_discovered_message_to_l2 do |
||||
query = |
||||
from(msg in Message, |
||||
select: msg.originating_transaction_block_number, |
||||
where: msg.direction == :to_l2 and not is_nil(msg.originating_transaction_block_number), |
||||
order_by: [asc: msg.message_id], |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the earliest rollup block where an L2-to-L1 message was discovered. |
||||
|
||||
## Returns |
||||
- The number of rollup block, or `nil` if no L2-to-L1 messages are found. |
||||
""" |
||||
@spec rollup_block_of_earliest_discovered_message_from_l2() :: FullBlock.block_number() | nil |
||||
def rollup_block_of_earliest_discovered_message_from_l2 do |
||||
query = |
||||
from(msg in Message, |
||||
select: msg.originating_transaction_block_number, |
||||
where: msg.direction == :from_l2 and not is_nil(msg.originating_transaction_block_number), |
||||
order_by: [asc: msg.originating_transaction_block_number], |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the earliest rollup block where a completed L1-to-L2 message was discovered. |
||||
|
||||
## Returns |
||||
- The block number of the rollup block, or `nil` if no completed L1-to-L2 messages are found, |
||||
or if the rollup transaction that emitted the corresponding message has not been indexed yet. |
||||
""" |
||||
@spec rollup_block_of_earliest_discovered_message_to_l2() :: FullBlock.block_number() | nil |
||||
def rollup_block_of_earliest_discovered_message_to_l2 do |
||||
completion_tx_subquery = |
||||
from(msg in Message, |
||||
select: msg.completion_transaction_hash, |
||||
where: msg.direction == :to_l2 and not is_nil(msg.completion_transaction_hash), |
||||
order_by: [asc: msg.message_id], |
||||
limit: 1 |
||||
) |
||||
|
||||
query = |
||||
from(tx in Transaction, |
||||
select: tx.block_number, |
||||
where: tx.hash == subquery(completion_tx_subquery), |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the latest L1 block where the commitment transaction with a batch was included. |
||||
|
||||
As per the Arbitrum rollup nature, from the indexer's point of view, a batch does not exist until |
||||
the commitment transaction is submitted to L1. Therefore, the situation where a batch exists but |
||||
there is no commitment transaction is not possible. |
||||
|
||||
## Returns |
||||
- The number of the L1 block, or `nil` if no rollup batches are found, or if the association between the batch |
||||
and the commitment transaction has been broken due to database inconsistency. |
||||
""" |
||||
@spec l1_block_of_latest_committed_batch() :: FullBlock.block_number() | nil |
||||
def l1_block_of_latest_committed_batch do |
||||
query = |
||||
from(batch in L1Batch, |
||||
order_by: [desc: batch.number], |
||||
limit: 1 |
||||
) |
||||
|
||||
case query |
||||
# :required is used since the situation when commit transaction is not found is not possible |
||||
|> Chain.join_associations(%{:commitment_transaction => :required}) |
||||
|> Repo.one() do |
||||
nil -> nil |
||||
batch -> batch.commitment_transaction.block_number |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the earliest L1 block where the commitment transaction with a batch was included. |
||||
|
||||
As per the Arbitrum rollup nature, from the indexer's point of view, a batch does not exist until |
||||
the commitment transaction is submitted to L1. Therefore, the situation where a batch exists but |
||||
there is no commitment transaction is not possible. |
||||
|
||||
## Returns |
||||
- The number of the L1 block, or `nil` if no rollup batches are found, or if the association between the batch |
||||
and the commitment transaction has been broken due to database inconsistency. |
||||
""" |
||||
@spec l1_block_of_earliest_committed_batch() :: FullBlock.block_number() | nil |
||||
def l1_block_of_earliest_committed_batch do |
||||
query = |
||||
from(batch in L1Batch, |
||||
order_by: [asc: batch.number], |
||||
limit: 1 |
||||
) |
||||
|
||||
case query |
||||
# :required is used since the situation when commit transaction is not found is not possible |
||||
|> Chain.join_associations(%{:commitment_transaction => :required}) |
||||
|> Repo.one() do |
||||
nil -> nil |
||||
batch -> batch.commitment_transaction.block_number |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the block number of the highest rollup block that has been included in a batch. |
||||
|
||||
## Returns |
||||
- The number of the highest rollup block included in a batch, or `nil` if no rollup batches are found. |
||||
""" |
||||
@spec highest_committed_block() :: FullBlock.block_number() | nil |
||||
def highest_committed_block do |
||||
query = |
||||
from(batch in L1Batch, |
||||
select: batch.end_block, |
||||
order_by: [desc: batch.number], |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Reads a list of L1 transactions by their hashes from the `arbitrum_lifecycle_l1_transactions` table. |
||||
|
||||
## Parameters |
||||
- `l1_tx_hashes`: A list of hashes to retrieve L1 transactions for. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.LifecycleTransaction` corresponding to the hashes from |
||||
the input list. The output list may be smaller than the input list. |
||||
""" |
||||
@spec lifecycle_transactions(maybe_improper_list(Hash.t(), [])) :: [LifecycleTransaction] |
||||
def lifecycle_transactions(l1_tx_hashes) when is_list(l1_tx_hashes) do |
||||
query = |
||||
from( |
||||
lt in LifecycleTransaction, |
||||
select: {lt.hash, lt.id}, |
||||
where: lt.hash in ^l1_tx_hashes |
||||
) |
||||
|
||||
Repo.all(query, timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Reads a list of transactions executing L2-to-L1 messages by their IDs. |
||||
|
||||
## Parameters |
||||
- `message_ids`: A list of IDs to retrieve executing transactions for. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.L1Execution` corresponding to the message IDs from |
||||
the input list. The output list may be smaller than the input list if some IDs do not |
||||
correspond to any existing transactions. |
||||
""" |
||||
@spec l1_executions(maybe_improper_list(non_neg_integer(), [])) :: [L1Execution] |
||||
def l1_executions(message_ids) when is_list(message_ids) do |
||||
query = |
||||
from( |
||||
ex in L1Execution, |
||||
where: ex.message_id in ^message_ids |
||||
) |
||||
|
||||
query |
||||
# :required is used since execution records in the table are created only when |
||||
# the corresponding execution transaction is indexed |
||||
|> Chain.join_associations(%{:execution_transaction => :required}) |
||||
|> Repo.all(timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Determines the next index for the L1 transaction available in the `arbitrum_lifecycle_l1_transactions` table. |
||||
|
||||
## Returns |
||||
- The next available index. If there are no L1 transactions imported yet, it will return `1`. |
||||
""" |
||||
@spec next_lifecycle_transaction_id() :: non_neg_integer |
||||
def next_lifecycle_transaction_id do |
||||
query = |
||||
from(lt in LifecycleTransaction, |
||||
select: lt.id, |
||||
order_by: [desc: lt.id], |
||||
limit: 1 |
||||
) |
||||
|
||||
last_id = |
||||
query |
||||
|> Repo.one() |
||||
|> Kernel.||(0) |
||||
|
||||
last_id + 1 |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves unfinalized L1 transactions from the `LifecycleTransaction` table that are |
||||
involved in changing the statuses of rollup blocks or transactions. |
||||
|
||||
An L1 transaction is considered unfinalized if it has not yet reached a state where |
||||
it is permanently included in the blockchain, meaning it is still susceptible to |
||||
potential reorganization or change. Transactions are evaluated against the `finalized_block` |
||||
parameter to determine their finalized status. |
||||
|
||||
## Parameters |
||||
- `finalized_block`: The L1 block number above which transactions are considered finalized. |
||||
Transactions in blocks higher than this number are not included in the results. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.LifecycleTransaction` representing unfinalized transactions, |
||||
or `[]` if no unfinalized transactions are found. |
||||
""" |
||||
@spec lifecycle_unfinalized_transactions(FullBlock.block_number()) :: [LifecycleTransaction] |
||||
def lifecycle_unfinalized_transactions(finalized_block) |
||||
when is_integer(finalized_block) and finalized_block >= 0 do |
||||
query = |
||||
from( |
||||
lt in LifecycleTransaction, |
||||
where: lt.block_number <= ^finalized_block and lt.status == :unfinalized |
||||
) |
||||
|
||||
Repo.all(query, timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Gets the rollup block number by the hash of the block. Lookup is performed only |
||||
for blocks explicitly included in a batch, i.e., the batch has been identified by |
||||
the corresponding fetcher. The function may return `nil` as a successful response |
||||
if the batch containing the rollup block has not been indexed yet. |
||||
|
||||
## Parameters |
||||
- `block_hash`: The hash of a block included in the batch. |
||||
|
||||
## Returns |
||||
- the number of the rollup block corresponding to the given hash or `nil` if the |
||||
block or batch were not indexed yet. |
||||
""" |
||||
@spec rollup_block_hash_to_num(binary()) :: FullBlock.block_number() | nil |
||||
def rollup_block_hash_to_num(block_hash) when is_binary(block_hash) do |
||||
query = |
||||
from( |
||||
fb in FullBlock, |
||||
inner_join: rb in BatchBlock, |
||||
on: rb.block_number == fb.number, |
||||
select: fb.number, |
||||
where: fb.hash == ^block_hash |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Checks if the numbers from the provided list correspond to the numbers of indexed batches. |
||||
|
||||
## Parameters |
||||
- `batches_numbers`: The list of batch numbers. |
||||
|
||||
## Returns |
||||
- A list of batch numbers that are indexed and match the provided list, or `[]` |
||||
if none of the batch numbers in the provided list exist in the database. The output list |
||||
may be smaller than the input list. |
||||
""" |
||||
@spec batches_exist(maybe_improper_list(non_neg_integer(), [])) :: [non_neg_integer] |
||||
def batches_exist(batches_numbers) when is_list(batches_numbers) do |
||||
query = |
||||
from( |
||||
batch in L1Batch, |
||||
select: batch.number, |
||||
where: batch.number in ^batches_numbers |
||||
) |
||||
|
||||
query |
||||
|> Repo.all(timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the batch in which the rollup block, identified by the given block number, was included. |
||||
|
||||
## Parameters |
||||
- `number`: The number of a rollup block. |
||||
|
||||
## Returns |
||||
- An instance of `Explorer.Chain.Arbitrum.L1Batch` representing the batch containing |
||||
the specified rollup block number, or `nil` if no corresponding batch is found. |
||||
""" |
||||
@spec get_batch_by_rollup_block_number(FullBlock.block_number()) :: L1Batch | nil |
||||
def get_batch_by_rollup_block_number(number) |
||||
when is_integer(number) and number >= 0 do |
||||
query = |
||||
from(batch in L1Batch, |
||||
# end_block has higher number than start_block |
||||
where: batch.end_block >= ^number and batch.start_block <= ^number |
||||
) |
||||
|
||||
query |
||||
# :required is used since the situation when commit transaction is not found is not possible |
||||
|> Chain.join_associations(%{:commitment_transaction => :required}) |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the L1 block number where the confirmation transaction of the highest confirmed rollup block was included. |
||||
|
||||
## Returns |
||||
- The L1 block number if a confirmed rollup block is found and the confirmation transaction is indexed; |
||||
`nil` if no confirmed rollup blocks are found or if there is a database inconsistency. |
||||
""" |
||||
@spec l1_block_of_latest_confirmed_block() :: FullBlock.block_number() | nil |
||||
def l1_block_of_latest_confirmed_block do |
||||
query = |
||||
from( |
||||
rb in BatchBlock, |
||||
where: not is_nil(rb.confirmation_id), |
||||
order_by: [desc: rb.block_number], |
||||
limit: 1 |
||||
) |
||||
|
||||
case query |
||||
# :required is used since existence of the confirmation id is checked above |
||||
|> Chain.join_associations(%{:confirmation_transaction => :required}) |
||||
|> Repo.one() do |
||||
nil -> |
||||
nil |
||||
|
||||
block -> |
||||
case block.confirmation_transaction do |
||||
# `nil` and `%Ecto.Association.NotLoaded{}` indicate DB inconsistency |
||||
nil -> nil |
||||
%Ecto.Association.NotLoaded{} -> nil |
||||
confirmation_transaction -> confirmation_transaction.block_number |
||||
end |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the highest confirmed rollup block. |
||||
|
||||
## Returns |
||||
- The number of the highest confirmed rollup block, or `nil` if no confirmed rollup blocks are found. |
||||
""" |
||||
@spec highest_confirmed_block() :: FullBlock.block_number() | nil |
||||
def highest_confirmed_block do |
||||
query = |
||||
from( |
||||
rb in BatchBlock, |
||||
where: not is_nil(rb.confirmation_id), |
||||
select: rb.block_number, |
||||
order_by: [desc: rb.block_number], |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the latest L1 block where a transaction executing an L2-to-L1 message was discovered. |
||||
|
||||
## Returns |
||||
- The number of the latest L1 block with an executing transaction for an L2-to-L1 message, or `nil` if no such transactions are found. |
||||
""" |
||||
@spec l1_block_of_latest_execution() :: FullBlock.block_number() | nil |
||||
def l1_block_of_latest_execution do |
||||
query = |
||||
from( |
||||
tx in LifecycleTransaction, |
||||
inner_join: ex in L1Execution, |
||||
on: tx.id == ex.execution_id, |
||||
select: tx.block_number, |
||||
order_by: [desc: tx.block_number], |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the number of the earliest L1 block where a transaction executing an L2-to-L1 message was discovered. |
||||
|
||||
## Returns |
||||
- The number of the earliest L1 block with an executing transaction for an L2-to-L1 message, or `nil` if no such transactions are found. |
||||
""" |
||||
@spec l1_block_of_earliest_execution() :: FullBlock.block_number() | nil |
||||
def l1_block_of_earliest_execution do |
||||
query = |
||||
from( |
||||
tx in LifecycleTransaction, |
||||
inner_join: ex in L1Execution, |
||||
on: tx.id == ex.execution_id, |
||||
select: tx.block_number, |
||||
order_by: [asc: tx.block_number], |
||||
limit: 1 |
||||
) |
||||
|
||||
query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves all unconfirmed rollup blocks within the specified range from `first_block` to `last_block`, |
||||
inclusive, where `first_block` is less than or equal to `last_block`. |
||||
|
||||
Since the function relies on the block data generated by the block fetcher, the returned list |
||||
may contain fewer blocks than actually exist if some of the blocks have not been indexed by the fetcher yet. |
||||
|
||||
## Parameters |
||||
- `first_block`: The rollup block number starting the lookup range. |
||||
- `last_block`:The rollup block number ending the lookup range. |
||||
|
||||
## Returns |
||||
- A list of maps containing the batch number, rollup block number and hash for each |
||||
unconfirmed block within the range. Returns `[]` if no unconfirmed blocks are found |
||||
within the range, or if the block fetcher has not indexed them. |
||||
""" |
||||
@spec unconfirmed_rollup_blocks(FullBlock.block_number(), FullBlock.block_number()) :: [BatchBlock] |
||||
def unconfirmed_rollup_blocks(first_block, last_block) |
||||
when is_integer(first_block) and first_block >= 0 and |
||||
is_integer(last_block) and first_block <= last_block do |
||||
query = |
||||
from( |
||||
rb in BatchBlock, |
||||
where: rb.block_number >= ^first_block and rb.block_number <= ^last_block and is_nil(rb.confirmation_id), |
||||
order_by: [asc: rb.block_number] |
||||
) |
||||
|
||||
Repo.all(query, timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Calculates the number of confirmed rollup blocks in the specified batch. |
||||
|
||||
## Parameters |
||||
- `batch_number`: The number of the batch for which the count of confirmed blocks is to be calculated. |
||||
|
||||
## Returns |
||||
- The number of confirmed blocks in the batch with the given number. |
||||
""" |
||||
@spec count_confirmed_rollup_blocks_in_batch(non_neg_integer()) :: non_neg_integer |
||||
def count_confirmed_rollup_blocks_in_batch(batch_number) |
||||
when is_integer(batch_number) and batch_number >= 0 do |
||||
query = |
||||
from( |
||||
rb in BatchBlock, |
||||
where: rb.batch_number == ^batch_number and not is_nil(rb.confirmation_id) |
||||
) |
||||
|
||||
Repo.aggregate(query, :count, timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves all L2-to-L1 messages with the specified status that originated in rollup blocks with numbers not higher than `block_number`. |
||||
|
||||
## Parameters |
||||
- `status`: The status of the messages to retrieve, such as `:initiated`, `:sent`, `:confirmed`, or `:relayed`. |
||||
- `block_number`: The number of a rollup block that limits the messages lookup. |
||||
|
||||
## Returns |
||||
- Instances of `Explorer.Chain.Arbitrum.Message` corresponding to the criteria, or `[]` if no messages |
||||
with the given status are found in the rollup blocks up to the specified number. |
||||
""" |
||||
@spec l2_to_l1_messages(:confirmed | :initiated | :relayed | :sent, FullBlock.block_number()) :: [ |
||||
Message |
||||
] |
||||
def l2_to_l1_messages(status, block_number) |
||||
when status in [:initiated, :sent, :confirmed, :relayed] and |
||||
is_integer(block_number) and |
||||
block_number >= 0 do |
||||
query = |
||||
from(msg in Message, |
||||
where: |
||||
msg.direction == :from_l2 and msg.originating_transaction_block_number <= ^block_number and |
||||
msg.status == ^status, |
||||
order_by: [desc: msg.message_id] |
||||
) |
||||
|
||||
Repo.all(query, timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the numbers of the L1 blocks containing the confirmation transactions |
||||
bounding the first interval where missed confirmation transactions could be found. |
||||
|
||||
The absence of a confirmation transaction is assumed based on the analysis of a |
||||
series of confirmed rollup blocks. For example, if blocks 0-3 are confirmed by transaction X, |
||||
blocks 7-9 by transaction Y, and blocks 12-15 by transaction Z, there are two gaps: |
||||
blocks 4-6 and 10-11. According to Arbitrum's nature, this indicates that the confirmation |
||||
transactions for blocks 6 and 11 have not yet been indexed. |
||||
|
||||
In the example above, the function will return the tuple with the numbers of the L1 blocks |
||||
where transactions Y and Z were included. |
||||
|
||||
## Returns |
||||
- A tuple of the L1 block numbers between which missing confirmation transactions are suspected, |
||||
or `nil` if no gaps in confirmed blocks are found or if there are no missed confirmation transactions. |
||||
""" |
||||
@spec l1_blocks_of_confirmations_bounding_first_unconfirmed_rollup_blocks_gap() :: |
||||
{FullBlock.block_number() | nil, FullBlock.block_number()} | nil |
||||
def l1_blocks_of_confirmations_bounding_first_unconfirmed_rollup_blocks_gap do |
||||
# The first subquery retrieves the numbers of confirmed rollup blocks. |
||||
rollup_blocks_query = |
||||
from( |
||||
rb in BatchBlock, |
||||
select: %{ |
||||
block_number: rb.block_number, |
||||
confirmation_id: rb.confirmation_id |
||||
}, |
||||
where: not is_nil(rb.confirmation_id) |
||||
) |
||||
|
||||
# The second subquery builds on the first one, grouping block numbers by their |
||||
# confirmation transactions. As a result, it identifies the starting and ending |
||||
# rollup blocks for every transaction. |
||||
confirmed_ranges_query = |
||||
from( |
||||
subquery in subquery(rollup_blocks_query), |
||||
select: %{ |
||||
confirmation_id: subquery.confirmation_id, |
||||
min_block_num: min(subquery.block_number), |
||||
max_block_num: max(subquery.block_number) |
||||
}, |
||||
group_by: subquery.confirmation_id |
||||
) |
||||
|
||||
# The third subquery utilizes the window function LAG to associate each confirmation |
||||
# transaction with the starting rollup block of the preceding transaction. |
||||
confirmed_combined_ranges_query = |
||||
from( |
||||
subquery in subquery(confirmed_ranges_query), |
||||
select: %{ |
||||
confirmation_id: subquery.confirmation_id, |
||||
min_block_num: subquery.min_block_num, |
||||
max_block_num: subquery.max_block_num, |
||||
prev_max_number: fragment("LAG(?, 1) OVER (ORDER BY ?)", subquery.max_block_num, subquery.min_block_num), |
||||
prev_confirmation_id: |
||||
fragment("LAG(?, 1) OVER (ORDER BY ?)", subquery.confirmation_id, subquery.min_block_num) |
||||
} |
||||
) |
||||
|
||||
# The final query identifies confirmation transactions for which the ending block does |
||||
# not precede the starting block of the subsequent confirmation transaction. |
||||
main_query = |
||||
from( |
||||
subquery in subquery(confirmed_combined_ranges_query), |
||||
inner_join: tx_cur in LifecycleTransaction, |
||||
on: subquery.confirmation_id == tx_cur.id, |
||||
left_join: tx_prev in LifecycleTransaction, |
||||
on: subquery.prev_confirmation_id == tx_prev.id, |
||||
select: {tx_prev.block_number, tx_cur.block_number}, |
||||
where: subquery.min_block_num - 1 != subquery.prev_max_number or is_nil(subquery.prev_max_number), |
||||
order_by: [desc: subquery.min_block_num], |
||||
limit: 1 |
||||
) |
||||
|
||||
main_query |
||||
|> Repo.one() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the count of cross-chain messages either sent to or from the rollup. |
||||
|
||||
## Parameters |
||||
- `direction`: A string that specifies the message direction; can be "from-rollup" or "to-rollup". |
||||
- `options`: A keyword list of options that may include whether to use a replica database. |
||||
|
||||
## Returns |
||||
- The total count of cross-chain messages. |
||||
""" |
||||
@spec messages_count(binary(), api?: boolean()) :: non_neg_integer() |
||||
def messages_count(direction, options) when direction == "from-rollup" and is_list(options) do |
||||
do_messages_count(:from_l2, options) |
||||
end |
||||
|
||||
def messages_count(direction, options) when direction == "to-rollup" and is_list(options) do |
||||
do_messages_count(:to_l2, options) |
||||
end |
||||
|
||||
# Counts the number of cross-chain messages based on the specified direction. |
||||
@spec do_messages_count(:from_l2 | :to_l2, api?: boolean()) :: non_neg_integer() |
||||
defp do_messages_count(direction, options) do |
||||
Message |
||||
|> where([msg], msg.direction == ^direction) |
||||
|> select_repo(options).aggregate(:count, timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves cross-chain messages based on the specified direction. |
||||
|
||||
This function constructs and executes a query to retrieve messages either sent |
||||
to or from the rollup layer, applying pagination options. These options dictate |
||||
not only the number of items to retrieve but also how many items to skip from |
||||
the top. |
||||
|
||||
## Parameters |
||||
- `direction`: A string that can be "from-rollup" or "to-rollup", translated internally to `:from_l2` or `:to_l2`. |
||||
- `options`: A keyword list specifying pagination details and database preferences. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.Message` entries. |
||||
""" |
||||
@spec messages(binary(), |
||||
paging_options: PagingOptions.t(), |
||||
api?: boolean() |
||||
) :: [Message] |
||||
def messages(direction, options) when direction == "from-rollup" do |
||||
do_messages(:from_l2, options) |
||||
end |
||||
|
||||
def messages(direction, options) when direction == "to-rollup" do |
||||
do_messages(:to_l2, options) |
||||
end |
||||
|
||||
# Executes the query to fetch cross-chain messages based on the specified direction. |
||||
# |
||||
# This function constructs and executes a query to retrieve messages either sent |
||||
# to or from the rollup layer, applying pagination options. These options dictate |
||||
# not only the number of items to retrieve but also how many items to skip from |
||||
# the top. |
||||
# |
||||
# ## Parameters |
||||
# - `direction`: Can be either `:from_l2` or `:to_l2`, indicating the direction of the messages. |
||||
# - `options`: A keyword list of options specifying pagination details and whether to use a replica database. |
||||
# |
||||
# ## Returns |
||||
# - A list of `Explorer.Chain.Arbitrum.Message` entries matching the specified direction. |
||||
@spec do_messages(:from_l2 | :to_l2, |
||||
paging_options: PagingOptions.t(), |
||||
api?: boolean() |
||||
) :: [Message] |
||||
defp do_messages(direction, options) do |
||||
base_query = |
||||
from(msg in Message, |
||||
where: msg.direction == ^direction, |
||||
order_by: [desc: msg.message_id] |
||||
) |
||||
|
||||
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) |
||||
|
||||
query = |
||||
base_query |
||||
|> page_messages(paging_options) |
||||
|> limit(^paging_options.page_size) |
||||
|
||||
select_repo(options).all(query) |
||||
end |
||||
|
||||
defp page_messages(query, %PagingOptions{key: nil}), do: query |
||||
|
||||
defp page_messages(query, %PagingOptions{key: {id}}) do |
||||
from(msg in query, where: msg.message_id < ^id) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a list of relayed L1 to L2 messages that have been completed. |
||||
|
||||
## Parameters |
||||
- `options`: A keyword list of options specifying whether to use a replica database and how pagination should be handled. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.Message` representing relayed messages from L1 to L2 that have been completed. |
||||
""" |
||||
@spec relayed_l1_to_l2_messages( |
||||
paging_options: PagingOptions.t(), |
||||
api?: boolean() |
||||
) :: [Message] |
||||
def relayed_l1_to_l2_messages(options) do |
||||
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) |
||||
|
||||
query = |
||||
from(msg in Message, |
||||
where: msg.direction == :to_l2 and not is_nil(msg.completion_transaction_hash), |
||||
order_by: [desc: msg.message_id], |
||||
limit: ^paging_options.page_size |
||||
) |
||||
|
||||
select_repo(options).all(query) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the total count of rollup batches indexed up to the current moment. |
||||
|
||||
This function uses an estimated count from system catalogs if available. |
||||
If the estimate is unavailable, it performs an exact count using an aggregate query. |
||||
|
||||
## Parameters |
||||
- `options`: A keyword list specifying options, including whether to use a replica database. |
||||
|
||||
## Returns |
||||
- The count of indexed batches. |
||||
""" |
||||
@spec batches_count(api?: boolean()) :: non_neg_integer() |
||||
def batches_count(options) do |
||||
Chain.get_table_rows_total_count(L1Batch, options) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a specific batch by its number or fetches the latest batch if `:latest` is specified. |
||||
|
||||
## Parameters |
||||
- `number`: Can be either the specific batch number or `:latest` to retrieve |
||||
the most recent batch in the database. |
||||
- `options`: A keyword list specifying the necessity for joining associations |
||||
and whether to use a replica database. |
||||
|
||||
## Returns |
||||
- `{:ok, Explorer.Chain.Arbitrum.L1Batch}` if the batch is found. |
||||
- `{:error, :not_found}` if no batch with the specified number exists. |
||||
""" |
||||
def batch(number, options) |
||||
|
||||
@spec batch(:latest, api?: boolean()) :: {:error, :not_found} | {:ok, L1Batch} |
||||
def batch(:latest, options) do |
||||
L1Batch |
||||
|> order_by(desc: :number) |
||||
|> limit(1) |
||||
|> select_repo(options).one() |
||||
|> case do |
||||
nil -> {:error, :not_found} |
||||
batch -> {:ok, batch} |
||||
end |
||||
end |
||||
|
||||
@spec batch(binary() | non_neg_integer(), |
||||
necessity_by_association: %{atom() => :optional | :required}, |
||||
api?: boolean() |
||||
) :: {:error, :not_found} | {:ok, L1Batch} |
||||
def batch(number, options) do |
||||
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||
|
||||
L1Batch |
||||
|> where(number: ^number) |
||||
|> Chain.join_associations(necessity_by_association) |
||||
|> select_repo(options).one() |
||||
|> case do |
||||
nil -> {:error, :not_found} |
||||
batch -> {:ok, batch} |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a list of batches from the database. |
||||
|
||||
This function constructs and executes a query to retrieve batches based on provided |
||||
pagination options. These options dictate not only the number of items to retrieve |
||||
but also how many items to skip from the top. If the `committed?` option is set to true, |
||||
it returns the ten most recent committed batches; otherwise, it fetches batches as |
||||
dictated by other pagination parameters. |
||||
|
||||
## Parameters |
||||
- `options`: A keyword list of options specifying pagination, necessity for joining associations, |
||||
and whether to use a replica database. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.L1Batch` entries, filtered and ordered according to the provided options. |
||||
""" |
||||
@spec batches( |
||||
necessity_by_association: %{atom() => :optional | :required}, |
||||
committed?: boolean(), |
||||
paging_options: PagingOptions.t(), |
||||
api?: boolean() |
||||
) :: [L1Batch] |
||||
def batches(options) do |
||||
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||
|
||||
base_query = |
||||
from(batch in L1Batch, |
||||
order_by: [desc: batch.number] |
||||
) |
||||
|
||||
query = |
||||
if Keyword.get(options, :committed?, false) do |
||||
base_query |
||||
|> Chain.join_associations(necessity_by_association) |
||||
|> where([batch], not is_nil(batch.commitment_id) and batch.commitment_id > 0) |
||||
|> limit(10) |
||||
else |
||||
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) |
||||
|
||||
base_query |
||||
|> Chain.join_associations(necessity_by_association) |
||||
|> page_batches(paging_options) |
||||
|> limit(^paging_options.page_size) |
||||
end |
||||
|
||||
select_repo(options).all(query) |
||||
end |
||||
|
||||
defp page_batches(query, %PagingOptions{key: nil}), do: query |
||||
|
||||
defp page_batches(query, %PagingOptions{key: {number}}) do |
||||
from(batch in query, where: batch.number < ^number) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a list of rollup transactions included in a specific batch. |
||||
|
||||
## Parameters |
||||
- `batch_number`: The batch number whose transactions were included in L1. |
||||
- `options`: A keyword list specifying options, including whether to use a replica database. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.BatchTransaction` entries belonging to the specified batch. |
||||
""" |
||||
@spec batch_transactions(non_neg_integer() | binary(), api?: boolean()) :: [BatchTransaction] |
||||
def batch_transactions(batch_number, options) do |
||||
query = from(tx in BatchTransaction, where: tx.batch_number == ^batch_number) |
||||
|
||||
select_repo(options).all(query) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a list of rollup blocks included in a specific batch. |
||||
|
||||
This function constructs and executes a database query to retrieve a list of rollup blocks, |
||||
considering pagination options specified in the `options` parameter. These options dictate |
||||
the number of items to retrieve and how many items to skip from the top. |
||||
|
||||
## Parameters |
||||
- `batch_number`: The batch number whose transactions are included on L1. |
||||
- `options`: A keyword list of options specifying pagination, association necessity, and |
||||
whether to use a replica database. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Block` entries belonging to the specified batch. |
||||
""" |
||||
@spec batch_blocks(non_neg_integer() | binary(), |
||||
necessity_by_association: %{atom() => :optional | :required}, |
||||
api?: boolean(), |
||||
paging_options: PagingOptions.t() |
||||
) :: [FullBlock] |
||||
def batch_blocks(batch_number, options) do |
||||
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) |
||||
|
||||
query = |
||||
from( |
||||
fb in FullBlock, |
||||
inner_join: rb in BatchBlock, |
||||
on: fb.number == rb.block_number, |
||||
select: fb, |
||||
where: fb.consensus == true and rb.batch_number == ^batch_number |
||||
) |
||||
|
||||
query |
||||
|> FullBlock.block_type_filter("Block") |
||||
|> page_blocks(paging_options) |
||||
|> limit(^paging_options.page_size) |
||||
|> order_by(desc: :number) |
||||
|> Chain.join_associations(necessity_by_association) |
||||
|> select_repo(options).all() |
||||
end |
||||
|
||||
defp page_blocks(query, %PagingOptions{key: nil}), do: query |
||||
|
||||
defp page_blocks(query, %PagingOptions{key: {block_number}}) do |
||||
where(query, [block], block.number < ^block_number) |
||||
end |
||||
end |
@ -0,0 +1,104 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Arbitrum.BatchBlocks do |
||||
@moduledoc """ |
||||
Bulk imports of Explorer.Chain.Arbitrum.BatchBlock. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Arbitrum.BatchBlock |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [BatchBlock.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: BatchBlock |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :arbitrum_batch_blocks |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_arbitrum_batch_blocks, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:arbitrum_batch_blocks, |
||||
:arbitrum_batch_blocks |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [BatchBlock.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do |
||||
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) |
||||
|
||||
# Enforce Arbitrum.BatchBlock ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = Enum.sort_by(changes_list, & &1.block_number) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: BatchBlock, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: :block_number, |
||||
on_conflict: on_conflict |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
|
||||
defp default_on_conflict do |
||||
from( |
||||
tb in BatchBlock, |
||||
update: [ |
||||
set: [ |
||||
# don't update `block_number` as it is a primary key and used for the conflict target |
||||
batch_number: fragment("EXCLUDED.batch_number"), |
||||
confirmation_id: fragment("EXCLUDED.confirmation_id"), |
||||
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", tb.inserted_at), |
||||
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", tb.updated_at) |
||||
] |
||||
], |
||||
where: |
||||
fragment( |
||||
"(EXCLUDED.batch_number, EXCLUDED.confirmation_id) IS DISTINCT FROM (?, ?)", |
||||
tb.batch_number, |
||||
tb.confirmation_id |
||||
) |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,79 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Arbitrum.BatchTransactions do |
||||
@moduledoc """ |
||||
Bulk imports of Explorer.Chain.Arbitrum.BatchTransaction. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Arbitrum.BatchTransaction |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [BatchTransaction.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: BatchTransaction |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :arbitrum_batch_transactions |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_arbitrum_batch_transactions, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:arbitrum_batch_transactions, |
||||
:arbitrum_batch_transactions |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [BatchTransaction.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do |
||||
# Enforce Arbitrum.BatchTransaction ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = Enum.sort_by(changes_list, & &1.tx_hash) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: BatchTransaction, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: :tx_hash, |
||||
on_conflict: :nothing |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
end |
@ -0,0 +1,112 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Arbitrum.L1Batches do |
||||
@moduledoc """ |
||||
Bulk imports of Explorer.Chain.Arbitrum.L1Batch. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Arbitrum.L1Batch |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [L1Batch.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: L1Batch |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :arbitrum_l1_batches |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_arbitrum_l1_batches, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:arbitrum_l1_batches, |
||||
:arbitrum_l1_batches |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [L1Batch.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do |
||||
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) |
||||
|
||||
# Enforce Arbitrum.L1Batch ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = Enum.sort_by(changes_list, & &1.number) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: L1Batch, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: :number, |
||||
on_conflict: on_conflict |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
|
||||
defp default_on_conflict do |
||||
from( |
||||
tb in L1Batch, |
||||
update: [ |
||||
set: [ |
||||
# don't update `number` as it is a primary key and used for the conflict target |
||||
transactions_count: fragment("EXCLUDED.transactions_count"), |
||||
start_block: fragment("EXCLUDED.start_block"), |
||||
end_block: fragment("EXCLUDED.end_block"), |
||||
before_acc: fragment("EXCLUDED.before_acc"), |
||||
after_acc: fragment("EXCLUDED.after_acc"), |
||||
commitment_id: fragment("EXCLUDED.commitment_id"), |
||||
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", tb.inserted_at), |
||||
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", tb.updated_at) |
||||
] |
||||
], |
||||
where: |
||||
fragment( |
||||
"(EXCLUDED.transactions_count, EXCLUDED.start_block, EXCLUDED.end_block, EXCLUDED.before_acc, EXCLUDED.after_acc, EXCLUDED.commitment_id) IS DISTINCT FROM (?, ?, ?, ?, ?, ?)", |
||||
tb.transactions_count, |
||||
tb.start_block, |
||||
tb.end_block, |
||||
tb.before_acc, |
||||
tb.after_acc, |
||||
tb.commitment_id |
||||
) |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,102 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Arbitrum.L1Executions do |
||||
@moduledoc """ |
||||
Bulk imports of Explorer.Chain.Arbitrum.L1Execution. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Arbitrum.L1Execution |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [L1Execution.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: L1Execution |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :arbitrum_l1_executions |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_arbitrum_l1_executions, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:arbitrum_l1_executions, |
||||
:arbitrum_l1_executions |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [L1Execution.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do |
||||
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) |
||||
|
||||
# Enforce Arbitrum.L1Execution ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = Enum.sort_by(changes_list, & &1.message_id) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: L1Execution, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: :message_id, |
||||
on_conflict: on_conflict |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
|
||||
defp default_on_conflict do |
||||
from( |
||||
tb in L1Execution, |
||||
update: [ |
||||
set: [ |
||||
# don't update `message_id` as it is a primary key and used for the conflict target |
||||
execution_id: fragment("EXCLUDED.execution_id"), |
||||
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", tb.inserted_at), |
||||
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", tb.updated_at) |
||||
] |
||||
], |
||||
where: |
||||
fragment( |
||||
"(EXCLUDED.execution_id) IS DISTINCT FROM (?)", |
||||
tb.execution_id |
||||
) |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,107 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Arbitrum.LifecycleTransactions do |
||||
@moduledoc """ |
||||
Bulk imports of Explorer.Chain.Arbitrum.LifecycleTransaction. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Arbitrum.LifecycleTransaction |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [LifecycleTransaction.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: LifecycleTransaction |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :arbitrum_lifecycle_transactions |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_arbitrum_lifecycle_transactions, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:arbitrum_lifecycle_transactions, |
||||
:arbitrum_lifecycle_transactions |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [LifecycleTransaction.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do |
||||
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) |
||||
|
||||
# Enforce Arbitrum.LifecycleTransaction ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = Enum.sort_by(changes_list, & &1.id) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: LifecycleTransaction, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: :hash, |
||||
on_conflict: on_conflict |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
|
||||
defp default_on_conflict do |
||||
from( |
||||
tx in LifecycleTransaction, |
||||
update: [ |
||||
set: [ |
||||
# don't update `id` as it is a primary key |
||||
# don't update `hash` as it is a unique index and used for the conflict target |
||||
timestamp: fragment("EXCLUDED.timestamp"), |
||||
block_number: fragment("EXCLUDED.block_number"), |
||||
status: fragment("GREATEST(?, EXCLUDED.status)", tx.status), |
||||
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", tx.inserted_at), |
||||
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", tx.updated_at) |
||||
] |
||||
], |
||||
where: |
||||
fragment( |
||||
"(EXCLUDED.timestamp, EXCLUDED.block_number, EXCLUDED.status) IS DISTINCT FROM (?, ?, ?)", |
||||
tx.timestamp, |
||||
tx.block_number, |
||||
tx.status |
||||
) |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,117 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Arbitrum.Messages do |
||||
@moduledoc """ |
||||
Bulk imports of Explorer.Chain.Arbitrum.Message. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Arbitrum.Message, as: CrosslevelMessage |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [CrosslevelMessage.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: CrosslevelMessage |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :arbitrum_messages |
||||
|
||||
@impl Import.Runner |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_arbitrum_messages, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:arbitrum_messages, |
||||
:arbitrum_messages |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [CrosslevelMessage.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do |
||||
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) |
||||
|
||||
# Enforce Message ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = Enum.sort_by(changes_list, &{&1.direction, &1.message_id}) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
conflict_target: [:direction, :message_id], |
||||
on_conflict: on_conflict, |
||||
for: CrosslevelMessage, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
|
||||
defp default_on_conflict do |
||||
from( |
||||
op in CrosslevelMessage, |
||||
update: [ |
||||
set: [ |
||||
# Don't update `direction` as it is part of the composite primary key and used for the conflict target |
||||
# Don't update `message_id` as it is part of the composite primary key and used for the conflict target |
||||
originator_address: fragment("COALESCE(EXCLUDED.originator_address, ?)", op.originator_address), |
||||
originating_transaction_hash: |
||||
fragment("COALESCE(EXCLUDED.originating_transaction_hash, ?)", op.originating_transaction_hash), |
||||
origination_timestamp: fragment("COALESCE(EXCLUDED.origination_timestamp, ?)", op.origination_timestamp), |
||||
originating_transaction_block_number: |
||||
fragment( |
||||
"COALESCE(EXCLUDED.originating_transaction_block_number, ?)", |
||||
op.originating_transaction_block_number |
||||
), |
||||
completion_transaction_hash: |
||||
fragment("COALESCE(EXCLUDED.completion_transaction_hash, ?)", op.completion_transaction_hash), |
||||
status: fragment("GREATEST(?, EXCLUDED.status)", op.status), |
||||
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", op.inserted_at), |
||||
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", op.updated_at) |
||||
] |
||||
], |
||||
where: |
||||
fragment( |
||||
"(EXCLUDED.originator_address, EXCLUDED.originating_transaction_hash, EXCLUDED.origination_timestamp, EXCLUDED.originating_transaction_block_number, EXCLUDED.completion_transaction_hash, EXCLUDED.status) IS DISTINCT FROM (?, ?, ?, ?, ?, ?)", |
||||
op.originator_address, |
||||
op.originating_transaction_hash, |
||||
op.origination_timestamp, |
||||
op.originating_transaction_block_number, |
||||
op.completion_transaction_hash, |
||||
op.status |
||||
) |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,124 @@ |
||||
defmodule Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
execute( |
||||
"CREATE TYPE arbitrum_messages_op_type AS ENUM ('to_l2', 'from_l2')", |
||||
"DROP TYPE arbitrum_messages_op_type" |
||||
) |
||||
|
||||
execute( |
||||
"CREATE TYPE arbitrum_messages_status AS ENUM ('initiated', 'sent', 'confirmed', 'relayed')", |
||||
"DROP TYPE arbitrum_messages_status" |
||||
) |
||||
|
||||
execute( |
||||
"CREATE TYPE l1_tx_status AS ENUM ('unfinalized', 'finalized')", |
||||
"DROP TYPE l1_tx_status" |
||||
) |
||||
|
||||
create table(:arbitrum_crosslevel_messages, primary_key: false) do |
||||
add(:direction, :arbitrum_messages_op_type, null: false, primary_key: true) |
||||
add(:message_id, :integer, null: false, primary_key: true) |
||||
add(:originator_address, :bytea, null: true) |
||||
add(:originating_transaction_hash, :bytea, null: true) |
||||
add(:origination_timestamp, :"timestamp without time zone", null: true) |
||||
add(:originating_transaction_block_number, :bigint, null: true) |
||||
add(:completion_transaction_hash, :bytea, null: true) |
||||
add(:status, :arbitrum_messages_status, null: false) |
||||
timestamps(null: false, type: :utc_datetime_usec) |
||||
end |
||||
|
||||
create(index(:arbitrum_crosslevel_messages, [:direction, :originating_transaction_block_number, :status])) |
||||
create(index(:arbitrum_crosslevel_messages, [:direction, :completion_transaction_hash])) |
||||
|
||||
create table(:arbitrum_lifecycle_l1_transactions, primary_key: false) do |
||||
add(:id, :integer, null: false, primary_key: true) |
||||
add(:hash, :bytea, null: false) |
||||
add(:block_number, :integer, null: false) |
||||
add(:timestamp, :"timestamp without time zone", null: false) |
||||
add(:status, :l1_tx_status, null: false) |
||||
timestamps(null: false, type: :utc_datetime_usec) |
||||
end |
||||
|
||||
create(unique_index(:arbitrum_lifecycle_l1_transactions, :hash)) |
||||
create(index(:arbitrum_lifecycle_l1_transactions, [:block_number, :status])) |
||||
|
||||
create table(:arbitrum_l1_executions, primary_key: false) do |
||||
add(:message_id, :integer, null: false, primary_key: true) |
||||
|
||||
add( |
||||
:execution_id, |
||||
references(:arbitrum_lifecycle_l1_transactions, on_delete: :restrict, on_update: :update_all, type: :integer), |
||||
null: false |
||||
) |
||||
|
||||
timestamps(null: false, type: :utc_datetime_usec) |
||||
end |
||||
|
||||
create table(:arbitrum_l1_batches, primary_key: false) do |
||||
add(:number, :integer, null: false, primary_key: true) |
||||
add(:transactions_count, :integer, null: false) |
||||
add(:start_block, :integer, null: false) |
||||
add(:end_block, :integer, null: false) |
||||
add(:before_acc, :bytea, null: false) |
||||
add(:after_acc, :bytea, null: false) |
||||
|
||||
add( |
||||
:commitment_id, |
||||
references(:arbitrum_lifecycle_l1_transactions, on_delete: :restrict, on_update: :update_all, type: :integer), |
||||
null: false |
||||
) |
||||
|
||||
timestamps(null: false, type: :utc_datetime_usec) |
||||
end |
||||
|
||||
create table(:arbitrum_batch_l2_blocks, primary_key: false) do |
||||
add( |
||||
:batch_number, |
||||
references(:arbitrum_l1_batches, |
||||
column: :number, |
||||
on_delete: :delete_all, |
||||
on_update: :update_all, |
||||
type: :integer |
||||
), |
||||
null: false |
||||
) |
||||
|
||||
add( |
||||
:confirmation_id, |
||||
references(:arbitrum_lifecycle_l1_transactions, on_delete: :restrict, on_update: :update_all, type: :integer), |
||||
null: true |
||||
) |
||||
|
||||
# Although it is possible to recover the block number from the block hash, |
||||
# it is more efficient to store it directly |
||||
# There could be no DB inconsistencies with `blocks` table caused be re-orgs |
||||
# because the blocks will appear in the table `arbitrum_batch_l2_blocks` |
||||
# only when they are included in the batch. |
||||
add(:block_number, :integer, null: false, primary_key: true) |
||||
timestamps(null: false, type: :utc_datetime_usec) |
||||
end |
||||
|
||||
create(index(:arbitrum_batch_l2_blocks, :batch_number)) |
||||
create(index(:arbitrum_batch_l2_blocks, :confirmation_id)) |
||||
|
||||
create table(:arbitrum_batch_l2_transactions, primary_key: false) do |
||||
add( |
||||
:batch_number, |
||||
references(:arbitrum_l1_batches, |
||||
column: :number, |
||||
on_delete: :delete_all, |
||||
on_update: :update_all, |
||||
type: :integer |
||||
), |
||||
null: false |
||||
) |
||||
|
||||
add(:tx_hash, :bytea, null: false, primary_key: true) |
||||
timestamps(null: false, type: :utc_datetime_usec) |
||||
end |
||||
|
||||
create(index(:arbitrum_batch_l2_transactions, :batch_number)) |
||||
end |
||||
end |
@ -0,0 +1,15 @@ |
||||
defmodule Explorer.Repo.Arbitrum.Migrations.ExtendTransactionAndBlockTables do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
alter table(:blocks) do |
||||
add(:send_count, :integer) |
||||
add(:send_root, :bytea) |
||||
add(:l1_block_number, :integer) |
||||
end |
||||
|
||||
alter table(:transactions) do |
||||
add(:gas_used_for_l1, :numeric, precision: 100) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,295 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Messaging do |
||||
@moduledoc """ |
||||
Provides functionality for filtering and handling messaging between Layer 1 (L1) and Layer 2 (L2) in the Arbitrum protocol. |
||||
|
||||
This module is responsible for identifying and processing messages that are transmitted |
||||
between L1 and L2. It includes functions to filter incoming logs and transactions to |
||||
find those that represent messages moving between the layers, and to handle the data of |
||||
these messages appropriately. |
||||
""" |
||||
|
||||
import EthereumJSONRPC, only: [quantity_to_integer: 1] |
||||
|
||||
import Explorer.Helper, only: [decode_data: 2] |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1, log_debug: 1] |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Utils.Db |
||||
|
||||
require Logger |
||||
|
||||
@l2_to_l1_event_unindexed_params [ |
||||
:address, |
||||
{:uint, 256}, |
||||
{:uint, 256}, |
||||
{:uint, 256}, |
||||
{:uint, 256}, |
||||
:bytes |
||||
] |
||||
|
||||
@type arbitrum_message :: %{ |
||||
direction: :to_l2 | :from_l2, |
||||
message_id: non_neg_integer(), |
||||
originator_address: binary(), |
||||
originating_transaction_hash: binary(), |
||||
origination_timestamp: DateTime.t(), |
||||
originating_transaction_block_number: non_neg_integer(), |
||||
completion_transaction_hash: binary(), |
||||
status: :initiated | :sent | :confirmed | :relayed |
||||
} |
||||
|
||||
@typep min_transaction :: %{ |
||||
:hash => binary(), |
||||
:type => non_neg_integer(), |
||||
optional(:request_id) => non_neg_integer(), |
||||
optional(any()) => any() |
||||
} |
||||
|
||||
@typep min_log :: %{ |
||||
:data => binary(), |
||||
:index => non_neg_integer(), |
||||
:first_topic => binary(), |
||||
:second_topic => binary(), |
||||
:third_topic => binary(), |
||||
:fourth_topic => binary(), |
||||
:address_hash => binary(), |
||||
:transaction_hash => binary(), |
||||
:block_hash => binary(), |
||||
:block_number => non_neg_integer(), |
||||
optional(any()) => any() |
||||
} |
||||
|
||||
@doc """ |
||||
Filters a list of rollup transactions to identify L1-to-L2 messages and composes a map for each with the related message information. |
||||
|
||||
This function filters through a list of rollup transactions, selecting those |
||||
with a non-nil `request_id`, indicating they are L1-to-L2 message completions. |
||||
These filtered transactions are then processed to construct a detailed message |
||||
structure for each. |
||||
|
||||
## Parameters |
||||
- `transactions`: A list of rollup transaction entries. |
||||
- `report`: An optional boolean flag (default `true`) that, when `true`, logs |
||||
the number of processed L1-to-L2 messages if any are found. |
||||
|
||||
## Returns |
||||
- A list of L1-to-L2 messages with detailed information and current status. Every |
||||
map in the list compatible with the database import operation. All messages in |
||||
this context are considered `:relayed` as they represent completed actions from |
||||
L1 to L2. |
||||
""" |
||||
@spec filter_l1_to_l2_messages(maybe_improper_list(min_transaction, [])) :: [arbitrum_message] |
||||
@spec filter_l1_to_l2_messages(maybe_improper_list(min_transaction, []), boolean()) :: [arbitrum_message] |
||||
def filter_l1_to_l2_messages(transactions, report \\ true) |
||||
when is_list(transactions) and is_boolean(report) do |
||||
messages = |
||||
transactions |
||||
|> Enum.filter(fn tx -> |
||||
tx[:request_id] != nil |
||||
end) |
||||
|> handle_filtered_l1_to_l2_messages() |
||||
|
||||
if report && not (messages == []) do |
||||
log_info("#{length(messages)} completions of L1-to-L2 messages will be imported") |
||||
end |
||||
|
||||
messages |
||||
end |
||||
|
||||
@doc """ |
||||
Filters logs for L2-to-L1 messages and composes a map for each with the related message information. |
||||
|
||||
This function filters a list of logs to identify those representing L2-to-L1 messages. |
||||
It checks each log against the ArbSys contract address and the `L2ToL1Tx` event |
||||
signature to determine if it corresponds to an L2-to-L1 message. |
||||
|
||||
## Parameters |
||||
- `logs`: A list of log entries. |
||||
|
||||
## Returns |
||||
- A list of L2-to-L1 messages with detailed information and current status. Each map |
||||
in the list is compatible with the database import operation. |
||||
""" |
||||
@spec filter_l2_to_l1_messages(maybe_improper_list(min_log, [])) :: [arbitrum_message] |
||||
def filter_l2_to_l1_messages(logs) when is_list(logs) do |
||||
arbsys_contract = Application.get_env(:indexer, __MODULE__)[:arbsys_contract] |
||||
|
||||
filtered_logs = |
||||
logs |
||||
|> Enum.filter(fn event -> |
||||
event.address_hash == arbsys_contract and event.first_topic == Db.l2_to_l1_event() |
||||
end) |
||||
|
||||
handle_filtered_l2_to_l1_messages(filtered_logs) |
||||
end |
||||
|
||||
@doc """ |
||||
Processes a list of filtered rollup transactions representing L1-to-L2 messages, constructing a detailed message structure for each. |
||||
|
||||
## Parameters |
||||
- `filtered_txs`: A list of rollup transaction entries, each representing an L1-to-L2 |
||||
message transaction. |
||||
|
||||
## Returns |
||||
- A list of L1-to-L2 messages with detailed information and current status. Every map |
||||
in the list compatible with the database import operation. All messages in this context |
||||
are considered `:relayed` as they represent completed actions from L1 to L2. |
||||
""" |
||||
@spec handle_filtered_l1_to_l2_messages(maybe_improper_list(min_transaction, [])) :: [arbitrum_message] |
||||
def handle_filtered_l1_to_l2_messages([]) do |
||||
[] |
||||
end |
||||
|
||||
def handle_filtered_l1_to_l2_messages(filtered_txs) when is_list(filtered_txs) do |
||||
filtered_txs |
||||
|> Enum.map(fn tx -> |
||||
log_debug("L1 to L2 message #{tx.hash} found with the type #{tx.type}") |
||||
|
||||
%{direction: :to_l2, message_id: tx.request_id, completion_transaction_hash: tx.hash, status: :relayed} |
||||
|> complete_to_params() |
||||
end) |
||||
end |
||||
|
||||
@doc """ |
||||
Processes a list of filtered logs representing L2-to-L1 messages, enriching and categorizing them based on their current state and optionally updating their execution status. |
||||
|
||||
This function takes filtered log events, typically representing L2-to-L1 messages, and |
||||
processes each to construct a comprehensive message structure. It also determines the |
||||
status of each message by comparing its block number against the highest committed and |
||||
confirmed block numbers. If a `caller` module is provided, it further updates the |
||||
messages' execution status. |
||||
|
||||
## Parameters |
||||
- `filtered_logs`: A list of log entries, each representing an L2-to-L1 message event. |
||||
- `caller`: An optional module that uses as a flag to determine if the discovered |
||||
should be checked for execution. |
||||
|
||||
## Returns |
||||
- A list of L2-to-L1 messages with detailed information and current status, ready for |
||||
database import. |
||||
""" |
||||
@spec handle_filtered_l2_to_l1_messages([min_log]) :: [arbitrum_message] |
||||
@spec handle_filtered_l2_to_l1_messages([min_log], module()) :: [arbitrum_message] |
||||
def handle_filtered_l2_to_l1_messages(filtered_logs, caller \\ nil) |
||||
|
||||
def handle_filtered_l2_to_l1_messages([], _) do |
||||
[] |
||||
end |
||||
|
||||
def handle_filtered_l2_to_l1_messages(filtered_logs, caller) when is_list(filtered_logs) do |
||||
# Get values before the loop parsing the events to reduce number of DB requests |
||||
highest_committed_block = Db.highest_committed_block(-1) |
||||
highest_confirmed_block = Db.highest_confirmed_block(-1) |
||||
|
||||
messages_map = |
||||
filtered_logs |
||||
|> Enum.reduce(%{}, fn event, messages_acc -> |
||||
log_debug("L2 to L1 message #{event.transaction_hash} found") |
||||
|
||||
{message_id, caller, blocknum, timestamp} = l2_to_l1_event_parse(event) |
||||
|
||||
message = |
||||
%{ |
||||
direction: :from_l2, |
||||
message_id: message_id, |
||||
originator_address: caller, |
||||
originating_transaction_hash: event.transaction_hash, |
||||
origination_timestamp: timestamp, |
||||
originating_transaction_block_number: blocknum, |
||||
status: status_l2_to_l1_message(blocknum, highest_committed_block, highest_confirmed_block) |
||||
} |
||||
|> complete_to_params() |
||||
|
||||
Map.put( |
||||
messages_acc, |
||||
message_id, |
||||
message |
||||
) |
||||
end) |
||||
|
||||
log_info("Origins of #{length(Map.values(messages_map))} L2-to-L1 messages will be imported") |
||||
|
||||
# The check if messages are executed is required only for the case when l2-to-l1 |
||||
# messages are found by block catchup fetcher |
||||
updated_messages_map = |
||||
case caller do |
||||
nil -> |
||||
messages_map |
||||
|
||||
_ -> |
||||
messages_map |
||||
|> find_and_update_executed_messages() |
||||
end |
||||
|
||||
updated_messages_map |
||||
|> Map.values() |
||||
end |
||||
|
||||
# Converts an incomplete message structure into a complete parameters map for database updates. |
||||
defp complete_to_params(incomplete) do |
||||
[ |
||||
:direction, |
||||
:message_id, |
||||
:originator_address, |
||||
:originating_transaction_hash, |
||||
:origination_timestamp, |
||||
:originating_transaction_block_number, |
||||
:completion_transaction_hash, |
||||
:status |
||||
] |
||||
|> Enum.reduce(%{}, fn key, out -> |
||||
Map.put(out, key, Map.get(incomplete, key)) |
||||
end) |
||||
end |
||||
|
||||
# Parses an L2-to-L1 event, extracting relevant information from the event's data. |
||||
defp l2_to_l1_event_parse(event) do |
||||
[ |
||||
caller, |
||||
arb_block_num, |
||||
_eth_block_num, |
||||
timestamp, |
||||
_callvalue, |
||||
_data |
||||
] = decode_data(event.data, @l2_to_l1_event_unindexed_params) |
||||
|
||||
position = quantity_to_integer(event.fourth_topic) |
||||
|
||||
{position, caller, arb_block_num, Timex.from_unix(timestamp)} |
||||
end |
||||
|
||||
# Determines the status of an L2-to-L1 message based on its block number and the highest |
||||
# committed and confirmed block numbers. |
||||
defp status_l2_to_l1_message(msg_block, highest_committed_block, highest_confirmed_block) do |
||||
cond do |
||||
highest_confirmed_block >= msg_block -> :confirmed |
||||
highest_committed_block >= msg_block -> :sent |
||||
true -> :initiated |
||||
end |
||||
end |
||||
|
||||
# Finds and updates the status of L2-to-L1 messages that have been executed on L1. |
||||
# This function iterates over the given messages, identifies those with corresponding L1 executions, |
||||
# and updates their `completion_transaction_hash` and `status` accordingly. |
||||
# |
||||
# ## Parameters |
||||
# - `messages`: A map where each key is a message ID, and each value is the message's details. |
||||
# |
||||
# ## Returns |
||||
# - The updated map of messages with the `completion_transaction_hash` and `status` fields updated |
||||
# for messages that have been executed. |
||||
defp find_and_update_executed_messages(messages) do |
||||
messages |
||||
|> Map.keys() |
||||
|> Db.l1_executions() |
||||
|> Enum.reduce(messages, fn execution, messages_acc -> |
||||
message = |
||||
messages_acc |
||||
|> Map.get(execution.message_id) |
||||
|> Map.put(:completion_transaction_hash, execution.execution_transaction.hash.bytes) |
||||
|> Map.put(:status, :relayed) |
||||
|
||||
Map.put(messages_acc, execution.message_id, message) |
||||
end) |
||||
end |
||||
end |
@ -0,0 +1,365 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.RollupMessagesCatchup do |
||||
@moduledoc """ |
||||
Manages the catch-up process for historical rollup messages between Layer 1 (L1) and Layer 2 (L2) within the Arbitrum network. |
||||
|
||||
This module aims to discover historical messages that were not captured by the block |
||||
fetcher or the catch-up block fetcher. This situation arises during the upgrade of an |
||||
existing instance of BlockScout (BS) that already has indexed blocks but lacks |
||||
a crosschain messages discovery mechanism. Therefore, it becomes necessary to traverse |
||||
the already indexed blocks to extract crosschain messages contained within them. |
||||
|
||||
The fetcher's operation cycle consists of five phases, initiated by sending specific |
||||
messages: |
||||
- `:wait_for_new_block`: Waits for the block fetcher to index new blocks before |
||||
proceeding with message discovery. |
||||
- `:init_worker`: Sets up the initial parameters for the message discovery process, |
||||
identifying the ending blocks for the search. |
||||
- `:historical_msg_from_l2` and `:historical_msg_to_l2`: Manage the discovery and |
||||
processing of messages sent from L2 to L1 and from L1 to L2, respectively. |
||||
- `:plan_next_iteration`: Schedules the next iteration of the catch-up process. |
||||
|
||||
Workflow diagram of the fetcher state changes: |
||||
|
||||
wait_for_new_block |
||||
| |
||||
V |
||||
init_worker |
||||
| |
||||
V |
||||
|-> historical_msg_from_l2 -> historical_msg_to_l2 -> plan_next_iteration ->| |
||||
|---------------------------------------------------------------------------| |
||||
|
||||
`historical_msg_from_l2` discovers L2-to-L1 messages by analyzing logs from already |
||||
indexed rollup transactions. Logs representing the `L2ToL1Tx` event are utilized |
||||
to construct messages. The current rollup state, including information about |
||||
committed batches and confirmed blocks, is used to assign the appropriate status |
||||
to the messages before importing them into the database. |
||||
|
||||
`historical_msg_to_l2` discovers L1-to-L2 messages by requesting rollup |
||||
transactions through RPC. Transactions containing a `requestId` in their body are |
||||
utilized to construct messages. These messages are marked as `:relayed`, indicating |
||||
that they have been successfully received on L2 and are considered completed, and |
||||
are then imported into the database. This approach is adopted because it parallels |
||||
the action of re-indexing existing transactions to include Arbitrum-specific fields, |
||||
which are absent in the currently indexed transactions. However, permanently adding |
||||
these fields to the database model for the sake of historical message catch-up is |
||||
impractical. Therefore, to avoid the extensive process of re-indexing and to |
||||
minimize changes to the database schema, fetching the required data directly from |
||||
an external node via RPC is preferred for historical message discovery. |
||||
""" |
||||
|
||||
use GenServer |
||||
use Indexer.Fetcher |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Helper, only: [increase_duration: 2] |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_warning: 1] |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Utils.Db |
||||
alias Indexer.Fetcher.Arbitrum.Workers.HistoricalMessagesOnL2 |
||||
|
||||
require Logger |
||||
|
||||
@wait_for_new_block_delay 15 |
||||
@release_cpu_delay 1 |
||||
|
||||
def child_spec(start_link_arguments) do |
||||
spec = %{ |
||||
id: __MODULE__, |
||||
start: {__MODULE__, :start_link, start_link_arguments}, |
||||
restart: :transient, |
||||
type: :worker |
||||
} |
||||
|
||||
Supervisor.child_spec(spec, []) |
||||
end |
||||
|
||||
def start_link(args, gen_server_options \\ []) do |
||||
GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) |
||||
end |
||||
|
||||
@impl GenServer |
||||
def init(args) do |
||||
Logger.metadata(fetcher: :arbitrum_bridge_l2_catchup) |
||||
|
||||
config_common = Application.get_all_env(:indexer)[Indexer.Fetcher.Arbitrum] |
||||
rollup_chunk_size = config_common[:rollup_chunk_size] |
||||
|
||||
config_tracker = Application.get_all_env(:indexer)[__MODULE__] |
||||
recheck_interval = config_tracker[:recheck_interval] |
||||
messages_to_l2_blocks_depth = config_tracker[:messages_to_l2_blocks_depth] |
||||
messages_from_l2_blocks_depth = config_tracker[:messages_to_l1_blocks_depth] |
||||
|
||||
Process.send(self(), :wait_for_new_block, []) |
||||
|
||||
{:ok, |
||||
%{ |
||||
config: %{ |
||||
rollup_rpc: %{ |
||||
json_rpc_named_arguments: args[:json_rpc_named_arguments], |
||||
chunk_size: rollup_chunk_size |
||||
}, |
||||
json_l2_rpc_named_arguments: args[:json_rpc_named_arguments], |
||||
recheck_interval: recheck_interval, |
||||
messages_to_l2_blocks_depth: messages_to_l2_blocks_depth, |
||||
messages_from_l2_blocks_depth: messages_from_l2_blocks_depth |
||||
}, |
||||
data: %{} |
||||
}} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_info({ref, _result}, state) do |
||||
Process.demonitor(ref, [:flush]) |
||||
{:noreply, state} |
||||
end |
||||
|
||||
# Waits for the next new block to be picked up by the block fetcher before initiating |
||||
# the worker for message discovery. |
||||
# |
||||
# This function checks if a new block has been indexed by the block fetcher since |
||||
# the start of the historical messages fetcher. It queries the database to find |
||||
# the closest block timestamped after this period. If a new block is found, it |
||||
# initiates the worker process for message discovery by sending the `:init_worker` |
||||
# message. If no new block is available, it reschedules itself to check again after |
||||
# a specified delay. |
||||
# |
||||
# The number of the new block indexed by the block fetcher will be used by the worker |
||||
# initializer to establish the end of the range where new messages should be discovered. |
||||
# |
||||
# ## Parameters |
||||
# - `:wait_for_new_block`: The message that triggers the waiting process. |
||||
# - `state`: The current state of the fetcher. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where the new indexed block number is stored, or retain |
||||
# the current state while awaiting new blocks. |
||||
@impl GenServer |
||||
def handle_info(:wait_for_new_block, %{data: _} = state) do |
||||
{time_of_start, interim_data} = |
||||
if is_nil(Map.get(state.data, :time_of_start)) do |
||||
now = DateTime.utc_now() |
||||
updated_data = Map.put(state.data, :time_of_start, now) |
||||
{now, updated_data} |
||||
else |
||||
{state.data.time_of_start, state.data} |
||||
end |
||||
|
||||
new_data = |
||||
case Db.closest_block_after_timestamp(time_of_start) do |
||||
{:ok, block} -> |
||||
Process.send(self(), :init_worker, []) |
||||
|
||||
interim_data |
||||
|> Map.put(:new_block, block) |
||||
|> Map.delete(:time_of_start) |
||||
|
||||
{:error, _} -> |
||||
log_warning("No progress of the block fetcher found") |
||||
Process.send_after(self(), :wait_for_new_block, :timer.seconds(@wait_for_new_block_delay)) |
||||
interim_data |
||||
end |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Sets the initial parameters for discovering historical messages. This function |
||||
# calculates the end blocks for both L1-to-L2 and L2-to-L1 message discovery |
||||
# processes based on th earliest messages already indexed. If no messages are |
||||
# available, the block number before the latest indexed block will be used. |
||||
# These end blocks are used to initiate the discovery process in subsequent iterations. |
||||
# |
||||
# After identifying the initial values, the function immediately transitions to |
||||
# the L2-to-L1 message discovery process by sending the `:historical_msg_from_l2` |
||||
# message. |
||||
# |
||||
# ## Parameters |
||||
# - `:init_worker`: The message that triggers the handler. |
||||
# - `state`: The current state of the fetcher. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where the end blocks for both L1-to-L2 and L2-to-L1 |
||||
# message discovery are established. |
||||
@impl GenServer |
||||
def handle_info(:init_worker, %{data: _} = state) do |
||||
historical_msg_from_l2_end_block = Db.rollup_block_to_discover_missed_messages_from_l2(state.data.new_block - 1) |
||||
historical_msg_to_l2_end_block = Db.rollup_block_to_discover_missed_messages_to_l2(state.data.new_block - 1) |
||||
|
||||
Process.send(self(), :historical_msg_from_l2, []) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: 0, |
||||
progressed: false, |
||||
historical_msg_from_l2_end_block: historical_msg_from_l2_end_block, |
||||
historical_msg_to_l2_end_block: historical_msg_to_l2_end_block |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Processes the next iteration of historical L2-to-L1 message discovery. |
||||
# |
||||
# This function uses the results from the previous iteration to set the end block |
||||
# for the current message discovery iteration. It identifies the start block and |
||||
# requests rollup logs within the specified range to explore `L2ToL1Tx` events. |
||||
# Discovered events are used to compose messages to be stored in the database. |
||||
# Before being stored in the database, each message is assigned the appropriate |
||||
# status based on the current state of the rollup. |
||||
# |
||||
# After importing the messages, the function immediately switches to the process |
||||
# of L1-to-L2 message discovery for the next range of blocks by sending |
||||
# the `:historical_msg_to_l2` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:historical_msg_from_l2`: The message triggering the handler. |
||||
# - `state`: The current state of the fetcher containing necessary data like |
||||
# the end block identified after the previous iteration of historical |
||||
# message discovery from L2. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where the end block for the next L2-to-L1 message |
||||
# discovery iteration is updated based on the results of the current iteration. |
||||
@impl GenServer |
||||
def handle_info( |
||||
:historical_msg_from_l2, |
||||
%{ |
||||
data: %{duration: _, historical_msg_from_l2_end_block: _, progressed: _} |
||||
} = state |
||||
) do |
||||
end_block = state.data.historical_msg_from_l2_end_block |
||||
|
||||
{handle_duration, {:ok, start_block}} = |
||||
:timer.tc(&HistoricalMessagesOnL2.discover_historical_messages_from_l2/2, [end_block, state]) |
||||
|
||||
Process.send(self(), :historical_msg_to_l2, []) |
||||
|
||||
progressed = state.data.progressed || (not is_nil(start_block) && start_block - 1 < end_block) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: increase_duration(state.data, handle_duration), |
||||
progressed: progressed, |
||||
historical_msg_from_l2_end_block: if(is_nil(start_block), do: nil, else: start_block - 1) |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Processes the next iteration of historical L1-to-L2 message discovery. |
||||
# |
||||
# This function uses the results from the previous iteration to set the end block for |
||||
# the current message discovery iteration. It identifies the start block and requests |
||||
# rollup blocks within the specified range through RPC to explore transactions |
||||
# containing a `requestId` in their body. This RPC request is necessary because the |
||||
# `requestId` field is not present in the transaction model of already indexed |
||||
# transactions in the database. The discovered transactions are then used to construct |
||||
# messages, which are subsequently stored in the database. These imported messages are |
||||
# marked as `:relayed`, signifying that they represent completed actions from L1 to L2. |
||||
# |
||||
# After importing the messages, the function immediately switches to the process |
||||
# of choosing a delay prior to the next iteration of historical messages discovery |
||||
# by sending the `:plan_next_iteration` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:historical_msg_to_l2`: The message triggering the handler. |
||||
# - `state`: The current state of the fetcher containing necessary data, like the end |
||||
# block identified after the previous iteration of historical message discovery. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where the end block for the next L1-to-L2 message discovery |
||||
# iteration is updated based on the results of the current iteration. |
||||
@impl GenServer |
||||
def handle_info( |
||||
:historical_msg_to_l2, |
||||
%{ |
||||
data: %{duration: _, historical_msg_to_l2_end_block: _, progressed: _} |
||||
} = state |
||||
) do |
||||
end_block = state.data.historical_msg_to_l2_end_block |
||||
|
||||
{handle_duration, {:ok, start_block}} = |
||||
:timer.tc(&HistoricalMessagesOnL2.discover_historical_messages_to_l2/2, [end_block, state]) |
||||
|
||||
Process.send(self(), :plan_next_iteration, []) |
||||
|
||||
progressed = state.data.progressed || (not is_nil(start_block) && start_block - 1 < end_block) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: increase_duration(state.data, handle_duration), |
||||
progressed: progressed, |
||||
historical_msg_to_l2_end_block: if(is_nil(start_block), do: nil, else: start_block - 1) |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Decides whether to stop or continue the fetcher based on the current state of message discovery. |
||||
# |
||||
# If both `historical_msg_from_l2_end_block` and `historical_msg_to_l2_end_block` are 0 or less, |
||||
# indicating that there are no more historical messages to fetch, the task is stopped with a normal |
||||
# termination. |
||||
# |
||||
# ## Parameters |
||||
# - `:plan_next_iteration`: The message that triggers this function. |
||||
# - `state`: The current state of the fetcher. |
||||
# |
||||
# ## Returns |
||||
# - `{:stop, :normal, state}`: Ends the fetcher's operation cleanly. |
||||
@impl GenServer |
||||
def handle_info( |
||||
:plan_next_iteration, |
||||
%{ |
||||
data: %{ |
||||
historical_msg_from_l2_end_block: from_l2_end_block, |
||||
historical_msg_to_l2_end_block: to_l2_end_block |
||||
} |
||||
} = state |
||||
) |
||||
when from_l2_end_block <= 0 and to_l2_end_block <= 0 do |
||||
{:stop, :normal, state} |
||||
end |
||||
|
||||
# Plans the next iteration for the historical messages discovery based on the state's `progressed` flag. |
||||
# |
||||
# If no progress was made (`progressed` is false), schedules the next check based |
||||
# on the `recheck_interval`, adjusted by the time already spent. If progress was |
||||
# made, it imposes a shorter delay to quickly check again, helping to reduce CPU |
||||
# usage during idle periods. |
||||
# |
||||
# The chosen delay is used to schedule the next iteration of historical messages discovery |
||||
# by sending `:historical_msg_from_l2`. |
||||
# |
||||
# ## Parameters |
||||
# - `:plan_next_iteration`: The message that triggers this function. |
||||
# - `state`: The current state of the fetcher containing both the fetcher configuration |
||||
# and data needed to determine the next steps. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, state}` where `state` contains the reset `duration` of the iteration and |
||||
# the flag if the messages discovery process `progressed`. |
||||
@impl GenServer |
||||
def handle_info( |
||||
:plan_next_iteration, |
||||
%{config: %{recheck_interval: _}, data: %{duration: _, progressed: _}} = state |
||||
) do |
||||
next_timeout = |
||||
if state.data.progressed do |
||||
# For the case when all historical messages are not received yet |
||||
# make a small delay to release CPU a bit |
||||
:timer.seconds(@release_cpu_delay) |
||||
else |
||||
max(state.config.recheck_interval - div(state.data.duration, 1000), 0) |
||||
end |
||||
|
||||
Process.send_after(self(), :historical_msg_from_l2, next_timeout) |
||||
|
||||
new_data = |
||||
state.data |
||||
|> Map.put(:duration, 0) |
||||
|> Map.put(:progressed, false) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
end |
@ -0,0 +1,459 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.TrackingBatchesStatuses do |
||||
@moduledoc """ |
||||
Manages the tracking and updating of the statuses of rollup batches, confirmations, and cross-chain message executions for an Arbitrum rollup. |
||||
|
||||
This module orchestrates the workflow for discovering new and historical |
||||
batches of rollup transactions, confirmations of rollup blocks, and |
||||
executions of L2-to-L1 messages. It ensures the accurate tracking and |
||||
updating of the rollup process stages. |
||||
|
||||
The fetcher's operation cycle begins with the `:init_worker` message, which |
||||
establishes the initial state with the necessary configuration. |
||||
|
||||
The process then progresses through a sequence of steps, each triggered by |
||||
specific messages: |
||||
- `:check_new_batches`: Discovers new batches of rollup transactions and |
||||
updates their statuses. |
||||
- `:check_new_confirmations`: Identifies new confirmations of rollup blocks |
||||
to update their statuses. |
||||
- `:check_new_executions`: Finds new executions of L2-to-L1 messages to |
||||
update their statuses. |
||||
- `:check_historical_batches`: Processes historical batches of rollup |
||||
transactions. |
||||
- `:check_historical_confirmations`: Handles historical confirmations of |
||||
rollup blocks. |
||||
- `:check_historical_executions`: Manages historical executions of L2-to-L1 |
||||
messages. |
||||
- `:check_lifecycle_txs_finalization`: Finalizes the status of lifecycle |
||||
transactions, confirming the blocks and messages involved. |
||||
|
||||
Discovery of rollup transaction batches is executed by requesting logs on L1 |
||||
that correspond to the `SequencerBatchDelivered` event emitted by the |
||||
Arbitrum `SequencerInbox` contract. |
||||
|
||||
Discovery of rollup block confirmations is executed by requesting logs on L1 |
||||
that correspond to the `SendRootUpdated` event emitted by the Arbitrum |
||||
`Outbox` contract. |
||||
|
||||
Discovery of the L2-to-L1 message executions occurs by requesting logs on L1 |
||||
that correspond to the `OutBoxTransactionExecuted` event emitted by the |
||||
Arbitrum `Outbox` contract. |
||||
|
||||
When processing batches or confirmations, the L2-to-L1 messages included in |
||||
the corresponding rollup blocks are updated to reflect their status changes. |
||||
""" |
||||
|
||||
use GenServer |
||||
use Indexer.Fetcher |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Workers.{L1Finalization, NewBatches, NewConfirmations, NewL1Executions} |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Helper, only: [increase_duration: 2] |
||||
|
||||
alias Indexer.Helper, as: IndexerHelper |
||||
alias Indexer.Fetcher.Arbitrum.Utils.{Db, Rpc} |
||||
|
||||
require Logger |
||||
|
||||
def child_spec(start_link_arguments) do |
||||
spec = %{ |
||||
id: __MODULE__, |
||||
start: {__MODULE__, :start_link, start_link_arguments}, |
||||
restart: :transient, |
||||
type: :worker |
||||
} |
||||
|
||||
Supervisor.child_spec(spec, []) |
||||
end |
||||
|
||||
def start_link(args, gen_server_options \\ []) do |
||||
GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) |
||||
end |
||||
|
||||
@impl GenServer |
||||
def init(args) do |
||||
Logger.metadata(fetcher: :arbitrum_batches_tracker) |
||||
|
||||
config_common = Application.get_all_env(:indexer)[Indexer.Fetcher.Arbitrum] |
||||
l1_rpc = config_common[:l1_rpc] |
||||
l1_rpc_block_range = config_common[:l1_rpc_block_range] |
||||
l1_rollup_address = config_common[:l1_rollup_address] |
||||
l1_rollup_init_block = config_common[:l1_rollup_init_block] |
||||
l1_start_block = config_common[:l1_start_block] |
||||
l1_rpc_chunk_size = config_common[:l1_rpc_chunk_size] |
||||
rollup_chunk_size = config_common[:rollup_chunk_size] |
||||
|
||||
config_tracker = Application.get_all_env(:indexer)[__MODULE__] |
||||
recheck_interval = config_tracker[:recheck_interval] |
||||
messages_to_blocks_shift = config_tracker[:messages_to_blocks_shift] |
||||
track_l1_tx_finalization = config_tracker[:track_l1_tx_finalization] |
||||
finalized_confirmations = config_tracker[:finalized_confirmations] |
||||
confirmation_batches_depth = config_tracker[:confirmation_batches_depth] |
||||
new_batches_limit = config_tracker[:new_batches_limit] |
||||
|
||||
Process.send(self(), :init_worker, []) |
||||
|
||||
{:ok, |
||||
%{ |
||||
config: %{ |
||||
l1_rpc: %{ |
||||
json_rpc_named_arguments: IndexerHelper.json_rpc_named_arguments(l1_rpc), |
||||
logs_block_range: l1_rpc_block_range, |
||||
chunk_size: l1_rpc_chunk_size, |
||||
track_finalization: track_l1_tx_finalization, |
||||
finalized_confirmations: finalized_confirmations |
||||
}, |
||||
rollup_rpc: %{ |
||||
json_rpc_named_arguments: args[:json_rpc_named_arguments], |
||||
chunk_size: rollup_chunk_size |
||||
}, |
||||
recheck_interval: recheck_interval, |
||||
l1_rollup_address: l1_rollup_address, |
||||
l1_start_block: l1_start_block, |
||||
l1_rollup_init_block: l1_rollup_init_block, |
||||
new_batches_limit: new_batches_limit, |
||||
messages_to_blocks_shift: messages_to_blocks_shift, |
||||
confirmation_batches_depth: confirmation_batches_depth |
||||
}, |
||||
data: %{} |
||||
}} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_info({ref, _result}, state) do |
||||
Process.demonitor(ref, [:flush]) |
||||
{:noreply, state} |
||||
end |
||||
|
||||
# Initializes the worker for discovering batches of rollup transactions, confirmations of rollup blocks, and executions of L2-to-L1 messages. |
||||
# |
||||
# This function sets up the initial state for the fetcher, identifying the |
||||
# starting blocks for new and historical discoveries of batches, confirmations, |
||||
# and executions. It also retrieves addresses for the Arbitrum Outbox and |
||||
# SequencerInbox contracts. |
||||
# |
||||
# After initializing these parameters, it immediately sends `:check_new_batches` |
||||
# to commence the fetcher loop. |
||||
# |
||||
# ## Parameters |
||||
# - `:init_worker`: The message triggering the initialization. |
||||
# - `state`: The current state of the process, containing initial configuration |
||||
# data. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with Arbitrum contract |
||||
# addresses and starting blocks for new and historical discoveries. |
||||
@impl GenServer |
||||
def handle_info( |
||||
:init_worker, |
||||
%{ |
||||
config: %{ |
||||
l1_rpc: %{json_rpc_named_arguments: json_l1_rpc_named_arguments}, |
||||
l1_rollup_address: l1_rollup_address |
||||
} |
||||
} = state |
||||
) do |
||||
%{outbox: outbox_address, sequencer_inbox: sequencer_inbox_address} = |
||||
Rpc.get_contracts_for_rollup( |
||||
l1_rollup_address, |
||||
:inbox_outbox, |
||||
json_l1_rpc_named_arguments |
||||
) |
||||
|
||||
l1_start_block = Rpc.get_l1_start_block(state.config.l1_start_block, json_l1_rpc_named_arguments) |
||||
|
||||
# TODO: it is necessary to develop a way to discover missed batches to cover the case |
||||
# when the batch #1, #2 and #4 are in DB, but #3 is not |
||||
# One of the approaches is to look deeper than the latest committed batch and |
||||
# check whether batches were already handled or not. |
||||
new_batches_start_block = Db.l1_block_to_discover_latest_committed_batch(l1_start_block) |
||||
historical_batches_end_block = Db.l1_block_to_discover_earliest_committed_batch(l1_start_block - 1) |
||||
|
||||
new_confirmations_start_block = Db.l1_block_of_latest_confirmed_block(l1_start_block) |
||||
|
||||
# TODO: it is necessary to develop a way to discover missed executions. |
||||
# One of the approaches is to look deeper than the latest execution and |
||||
# check whether executions were already handled or not. |
||||
new_executions_start_block = Db.l1_block_to_discover_latest_execution(l1_start_block) |
||||
historical_executions_end_block = Db.l1_block_to_discover_earliest_execution(l1_start_block - 1) |
||||
|
||||
Process.send(self(), :check_new_batches, []) |
||||
|
||||
new_state = |
||||
state |
||||
|> Map.put( |
||||
:config, |
||||
Map.merge(state.config, %{ |
||||
l1_start_block: l1_start_block, |
||||
l1_outbox_address: outbox_address, |
||||
l1_sequencer_inbox_address: sequencer_inbox_address |
||||
}) |
||||
) |
||||
|> Map.put( |
||||
:data, |
||||
Map.merge(state.data, %{ |
||||
new_batches_start_block: new_batches_start_block, |
||||
historical_batches_end_block: historical_batches_end_block, |
||||
new_confirmations_start_block: new_confirmations_start_block, |
||||
historical_confirmations_end_block: nil, |
||||
historical_confirmations_start_block: nil, |
||||
new_executions_start_block: new_executions_start_block, |
||||
historical_executions_end_block: historical_executions_end_block |
||||
}) |
||||
) |
||||
|
||||
{:noreply, new_state} |
||||
end |
||||
|
||||
# Initiates the process of discovering and handling new batches of rollup transactions. |
||||
# |
||||
# This function fetches logs within the calculated L1 block range to identify new |
||||
# batches of rollup transactions. The discovered batches and their corresponding |
||||
# rollup blocks and transactions are processed and linked. The L2-to-L1 messages |
||||
# included in these rollup blocks are also updated to reflect their commitment. |
||||
# |
||||
# After processing, it immediately transitions to checking new confirmations of |
||||
# rollup blocks by sending the `:check_new_confirmations` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_new_batches`: The message that triggers the function. |
||||
# - `state`: The current state of the fetcher, containing configuration and data |
||||
# needed for the discovery of new batches. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with the new start block for |
||||
# the next iteration of new batch discovery. |
||||
@impl GenServer |
||||
def handle_info(:check_new_batches, state) do |
||||
{handle_duration, {:ok, end_block}} = :timer.tc(&NewBatches.discover_new_batches/1, [state]) |
||||
|
||||
Process.send(self(), :check_new_confirmations, []) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: increase_duration(state.data, handle_duration), |
||||
new_batches_start_block: end_block + 1 |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Initiates the discovery and processing of new confirmations for rollup blocks. |
||||
# |
||||
# This function fetches logs within the calculated L1 block range to identify |
||||
# new confirmations for rollup blocks. The discovered confirmations are |
||||
# processed to update the status of rollup blocks and L2-to-L1 messages |
||||
# accordingly. |
||||
# |
||||
# After processing, it immediately transitions to discovering new executions |
||||
# of L2-to-L1 messages by sending the `:check_new_executions` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_new_confirmations`: The message that triggers the function. |
||||
# - `state`: The current state of the fetcher, containing configuration and |
||||
# data needed for the discovery of new rollup block confirmations. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with the new start |
||||
# block for the next iteration of new confirmation discovery. |
||||
@impl GenServer |
||||
def handle_info(:check_new_confirmations, state) do |
||||
{handle_duration, {retcode, end_block}} = :timer.tc(&NewConfirmations.discover_new_rollup_confirmation/1, [state]) |
||||
|
||||
Process.send(self(), :check_new_executions, []) |
||||
|
||||
updated_fields = |
||||
case retcode do |
||||
:ok -> %{} |
||||
_ -> %{historical_confirmations_end_block: nil, historical_confirmations_start_block: nil} |
||||
end |
||||
|> Map.merge(%{ |
||||
# credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart |
||||
duration: increase_duration(state.data, handle_duration), |
||||
new_confirmations_start_block: end_block + 1 |
||||
}) |
||||
|
||||
new_data = Map.merge(state.data, updated_fields) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Initiates the process of discovering and handling new executions for L2-to-L1 messages. |
||||
# |
||||
# This function identifies new executions of L2-to-L1 messages by fetching logs |
||||
# for the calculated L1 block range. It updates the status of these messages and |
||||
# links them with the corresponding lifecycle transactions. |
||||
# |
||||
# After processing, it immediately transitions to checking historical batches of |
||||
# rollup transaction by sending the `:check_historical_batches` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_new_executions`: The message that triggers the function. |
||||
# - `state`: The current state of the fetcher, containing configuration and data |
||||
# needed for the discovery of new message executions. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with the new start |
||||
# block for the next iteration of new message executions discovery. |
||||
@impl GenServer |
||||
def handle_info(:check_new_executions, state) do |
||||
{handle_duration, {:ok, end_block}} = :timer.tc(&NewL1Executions.discover_new_l1_messages_executions/1, [state]) |
||||
|
||||
Process.send(self(), :check_historical_batches, []) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: increase_duration(state.data, handle_duration), |
||||
new_executions_start_block: end_block + 1 |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Initiates the process of discovering and handling historical batches of rollup transactions. |
||||
# |
||||
# This function fetches logs within the calculated L1 block range to identify the |
||||
# historical batches of rollup transactions. After discovery the linkage between |
||||
# batches and the corresponding rollup blocks and transactions are build. The |
||||
# status of the L2-to-L1 messages included in the corresponding rollup blocks is |
||||
# also updated. |
||||
# |
||||
# After processing, it immediately transitions to checking historical |
||||
# confirmations of rollup blocks by sending the `:check_historical_confirmations` |
||||
# message. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_historical_batches`: The message that triggers the function. |
||||
# - `state`: The current state of the fetcher, containing configuration and data |
||||
# needed for the discovery of historical batches. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with the new end block |
||||
# for the next iteration of historical batch discovery. |
||||
@impl GenServer |
||||
def handle_info(:check_historical_batches, state) do |
||||
{handle_duration, {:ok, start_block}} = :timer.tc(&NewBatches.discover_historical_batches/1, [state]) |
||||
|
||||
Process.send(self(), :check_historical_confirmations, []) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: increase_duration(state.data, handle_duration), |
||||
historical_batches_end_block: start_block - 1 |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Initiates the process of discovering and handling historical confirmations of rollup blocks. |
||||
# |
||||
# This function fetches logs within the calculated range to identify the |
||||
# historical confirmations of rollup blocks. The discovered confirmations are |
||||
# processed to update the status of rollup blocks and L2-to-L1 messages |
||||
# accordingly. |
||||
# |
||||
# After processing, it immediately transitions to checking historical executions |
||||
# of L2-to-L1 messages by sending the `:check_historical_executions` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_historical_confirmations`: The message that triggers the function. |
||||
# - `state`: The current state of the fetcher, containing configuration and data |
||||
# needed for the discovery of historical confirmations. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with the new start and |
||||
# end blocks for the next iteration of historical confirmations discovery. |
||||
@impl GenServer |
||||
def handle_info(:check_historical_confirmations, state) do |
||||
{handle_duration, {retcode, {start_block, end_block}}} = |
||||
:timer.tc(&NewConfirmations.discover_historical_rollup_confirmation/1, [state]) |
||||
|
||||
Process.send(self(), :check_historical_executions, []) |
||||
|
||||
updated_fields = |
||||
case retcode do |
||||
:ok -> %{historical_confirmations_end_block: start_block - 1, historical_confirmations_start_block: end_block} |
||||
_ -> %{historical_confirmations_end_block: nil, historical_confirmations_start_block: nil} |
||||
end |
||||
|> Map.merge(%{ |
||||
# credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart |
||||
duration: increase_duration(state.data, handle_duration) |
||||
}) |
||||
|
||||
new_data = Map.merge(state.data, updated_fields) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Initiates the discovery and handling of historical L2-to-L1 message executions. |
||||
# |
||||
# This function discovers historical executions of L2-to-L1 messages by retrieving |
||||
# logs within a specified L1 block range. It updates their status accordingly and |
||||
# builds the link between the messages and the lifecycle transactions where they |
||||
# are executed. |
||||
# |
||||
# After processing, it immediately transitions to finalizing lifecycle transactions |
||||
# by sending the `:check_lifecycle_txs_finalization` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_historical_executions`: The message that triggers the function. |
||||
# - `state`: The current state of the fetcher, containing configuration and data |
||||
# needed for the discovery of historical executions. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with the new end block for |
||||
# the next iteration of historical executions. |
||||
@impl GenServer |
||||
def handle_info(:check_historical_executions, state) do |
||||
{handle_duration, {:ok, start_block}} = |
||||
:timer.tc(&NewL1Executions.discover_historical_l1_messages_executions/1, [state]) |
||||
|
||||
Process.send(self(), :check_lifecycle_txs_finalization, []) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: increase_duration(state.data, handle_duration), |
||||
historical_executions_end_block: start_block - 1 |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Handles the periodic finalization check of lifecycle transactions. |
||||
# |
||||
# This function updates the finalization status of lifecycle transactions based on |
||||
# the current state of the L1 blockchain. It discovers all transactions that are not |
||||
# yet finalized up to the `safe` L1 block and changes their status to `:finalized`. |
||||
# |
||||
# After processing, as the final handler in the loop, it schedules the |
||||
# `:check_new_batches` message to initiate the next iteration. The scheduling of this |
||||
# message is delayed to account for the time spent on the previous handlers' execution. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_lifecycle_txs_finalization`: The message that triggers the function. |
||||
# - `state`: The current state of the fetcher, containing the configuration needed for |
||||
# the lifecycle transactions status update. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is the updated state with the reset duration. |
||||
@impl GenServer |
||||
def handle_info(:check_lifecycle_txs_finalization, state) do |
||||
{handle_duration, _} = |
||||
if state.config.l1_rpc.track_finalization do |
||||
:timer.tc(&L1Finalization.monitor_lifecycle_txs/1, [state]) |
||||
else |
||||
{0, nil} |
||||
end |
||||
|
||||
next_timeout = max(state.config.recheck_interval - div(increase_duration(state.data, handle_duration), 1000), 0) |
||||
|
||||
Process.send_after(self(), :check_new_batches, next_timeout) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: 0 |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
end |
@ -0,0 +1,223 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.TrackingMessagesOnL1 do |
||||
@moduledoc """ |
||||
Manages the tracking and processing of new and historical cross-chain messages initiated on L1 for an Arbitrum rollup. |
||||
|
||||
This module is responsible for continuously monitoring and importing new messages |
||||
initiated from Layer 1 (L1) to Arbitrum's Layer 2 (L2), as well as discovering |
||||
and processing historical messages that were sent previously but have not yet |
||||
been processed. |
||||
|
||||
The fetcher's operation is divided into 3 phases, each initiated by sending |
||||
specific messages: |
||||
- `:init_worker`: Initializes the worker with the required configuration for message |
||||
tracking. |
||||
- `:check_new_msgs_to_rollup`: Processes new L1-to-L2 messages appearing on L1 as |
||||
the blockchain progresses. |
||||
- `:check_historical_msgs_to_rollup`: Retrieves historical L1-to-L2 messages that |
||||
were missed if the message synchronization process did not start from the |
||||
Arbitrum rollup's inception. |
||||
|
||||
While the `:init_worker` message is sent only once during the fetcher startup, |
||||
the subsequent sending of `:check_new_msgs_to_rollup` and |
||||
`:check_historical_msgs_to_rollup` forms the operation cycle of the fetcher. |
||||
|
||||
Discovery of L1-to-L2 messages is executed by requesting logs on L1 that correspond |
||||
to the `MessageDelivered` event emitted by the Arbitrum bridge contract. |
||||
Cross-chain messages are composed of information from the logs' data as well as from |
||||
the corresponding transaction details. To get the transaction details, RPC calls |
||||
`eth_getTransactionByHash` are made in chunks. |
||||
""" |
||||
|
||||
use GenServer |
||||
use Indexer.Fetcher |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Helper, only: [increase_duration: 2] |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 |
||||
|
||||
alias Indexer.Helper, as: IndexerHelper |
||||
alias Indexer.Fetcher.Arbitrum.Utils.{Db, Rpc} |
||||
|
||||
require Logger |
||||
|
||||
def child_spec(start_link_arguments) do |
||||
spec = %{ |
||||
id: __MODULE__, |
||||
start: {__MODULE__, :start_link, start_link_arguments}, |
||||
restart: :transient, |
||||
type: :worker |
||||
} |
||||
|
||||
Supervisor.child_spec(spec, []) |
||||
end |
||||
|
||||
def start_link(args, gen_server_options \\ []) do |
||||
GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) |
||||
end |
||||
|
||||
@impl GenServer |
||||
def init(args) do |
||||
Logger.metadata(fetcher: :arbitrum_bridge_l1) |
||||
|
||||
config_common = Application.get_all_env(:indexer)[Indexer.Fetcher.Arbitrum] |
||||
l1_rpc = config_common[:l1_rpc] |
||||
l1_rpc_block_range = config_common[:l1_rpc_block_range] |
||||
l1_rollup_address = config_common[:l1_rollup_address] |
||||
l1_rollup_init_block = config_common[:l1_rollup_init_block] |
||||
l1_start_block = config_common[:l1_start_block] |
||||
l1_rpc_chunk_size = config_common[:l1_rpc_chunk_size] |
||||
|
||||
config_tracker = Application.get_all_env(:indexer)[__MODULE__] |
||||
recheck_interval = config_tracker[:recheck_interval] |
||||
|
||||
Process.send(self(), :init_worker, []) |
||||
|
||||
{:ok, |
||||
%{ |
||||
config: %{ |
||||
json_l2_rpc_named_arguments: args[:json_rpc_named_arguments], |
||||
json_l1_rpc_named_arguments: IndexerHelper.json_rpc_named_arguments(l1_rpc), |
||||
recheck_interval: recheck_interval, |
||||
l1_rpc_chunk_size: l1_rpc_chunk_size, |
||||
l1_rpc_block_range: l1_rpc_block_range, |
||||
l1_rollup_address: l1_rollup_address, |
||||
l1_start_block: l1_start_block, |
||||
l1_rollup_init_block: l1_rollup_init_block |
||||
}, |
||||
data: %{} |
||||
}} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_info({ref, _result}, state) do |
||||
Process.demonitor(ref, [:flush]) |
||||
{:noreply, state} |
||||
end |
||||
|
||||
# Initializes the worker for discovering new and historical L1-to-L2 messages. |
||||
# |
||||
# This function prepares the initial parameters for the message discovery process. |
||||
# It fetches the Arbitrum bridge address and determines the starting block for |
||||
# new message discovery. If the starting block is not configured (set to a default |
||||
# value), the latest block number from L1 is used as the start. It also calculates |
||||
# the end block for historical message discovery. |
||||
# |
||||
# After setting these parameters, it immediately transitions to discovering new |
||||
# messages by sending the `:check_new_msgs_to_rollup` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:init_worker`: The message triggering the initialization. |
||||
# - `state`: The current state of the process, containing configuration for data |
||||
# initialization and further message discovery. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where `new_state` is updated with the bridge address, |
||||
# determined start block for new messages, and calculated end block for |
||||
# historical messages. |
||||
@impl GenServer |
||||
def handle_info( |
||||
:init_worker, |
||||
%{config: %{l1_rollup_address: _, json_l1_rpc_named_arguments: _, l1_start_block: _}, data: _} = state |
||||
) do |
||||
%{bridge: bridge_address} = |
||||
Rpc.get_contracts_for_rollup(state.config.l1_rollup_address, :bridge, state.config.json_l1_rpc_named_arguments) |
||||
|
||||
l1_start_block = Rpc.get_l1_start_block(state.config.l1_start_block, state.config.json_l1_rpc_named_arguments) |
||||
new_msg_to_l2_start_block = Db.l1_block_to_discover_latest_message_to_l2(l1_start_block) |
||||
historical_msg_to_l2_end_block = Db.l1_block_to_discover_earliest_message_to_l2(l1_start_block - 1) |
||||
|
||||
Process.send(self(), :check_new_msgs_to_rollup, []) |
||||
|
||||
new_state = |
||||
state |
||||
|> Map.put( |
||||
:config, |
||||
Map.merge(state.config, %{ |
||||
l1_start_block: l1_start_block, |
||||
l1_bridge_address: bridge_address |
||||
}) |
||||
) |
||||
|> Map.put( |
||||
:data, |
||||
Map.merge(state.data, %{ |
||||
new_msg_to_l2_start_block: new_msg_to_l2_start_block, |
||||
historical_msg_to_l2_end_block: historical_msg_to_l2_end_block |
||||
}) |
||||
) |
||||
|
||||
{:noreply, new_state} |
||||
end |
||||
|
||||
# Initiates the process to discover and handle new L1-to-L2 messages initiated from L1. |
||||
# |
||||
# This function discovers new messages from L1 to L2 by retrieving logs for the |
||||
# calculated L1 block range. Discovered events are used to compose messages, which |
||||
# are then stored in the database. |
||||
# |
||||
# After processing, the function immediately transitions to discovering historical |
||||
# messages by sending the `:check_historical_msgs_to_rollup` message. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_new_msgs_to_rollup`: The message that triggers the handler. |
||||
# - `state`: The current state of the fetcher, containing configuration and data |
||||
# needed for message discovery. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where the starting block for the next new L1-to-L2 |
||||
# message discovery iteration is updated based on the results of the current |
||||
# iteration. |
||||
@impl GenServer |
||||
def handle_info(:check_new_msgs_to_rollup, %{data: _} = state) do |
||||
{handle_duration, {:ok, end_block}} = |
||||
:timer.tc(&NewMessagesToL2.discover_new_messages_to_l2/1, [ |
||||
state |
||||
]) |
||||
|
||||
Process.send(self(), :check_historical_msgs_to_rollup, []) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: increase_duration(state.data, handle_duration), |
||||
new_msg_to_l2_start_block: end_block + 1 |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
|
||||
# Initiates the process to discover and handle historical L1-to-L2 messages initiated from L1. |
||||
# |
||||
# This function discovers historical messages by retrieving logs for a calculated L1 block range. |
||||
# The discovered events are then used to compose messages to be stored in the database. |
||||
# |
||||
# After processing, as it is the final handler in the loop, it schedules the |
||||
# `:check_new_msgs_to_rollup` message to initiate the next iteration. The scheduling of this |
||||
# message is delayed, taking into account the time spent on the previous handler's execution. |
||||
# |
||||
# ## Parameters |
||||
# - `:check_historical_msgs_to_rollup`: The message that triggers the handler. |
||||
# - `state`: The current state of the fetcher, containing configuration and data needed for |
||||
# message discovery. |
||||
# |
||||
# ## Returns |
||||
# - `{:noreply, new_state}` where the end block for the next L1-to-L2 message discovery |
||||
# iteration is updated based on the results of the current iteration. |
||||
@impl GenServer |
||||
def handle_info(:check_historical_msgs_to_rollup, %{config: %{recheck_interval: _}, data: _} = state) do |
||||
{handle_duration, {:ok, start_block}} = |
||||
:timer.tc(&NewMessagesToL2.discover_historical_messages_to_l2/1, [ |
||||
state |
||||
]) |
||||
|
||||
next_timeout = max(state.config.recheck_interval - div(increase_duration(state.data, handle_duration), 1000), 0) |
||||
|
||||
Process.send_after(self(), :check_new_msgs_to_rollup, next_timeout) |
||||
|
||||
new_data = |
||||
Map.merge(state.data, %{ |
||||
duration: 0, |
||||
historical_msg_to_l2_end_block: start_block - 1 |
||||
}) |
||||
|
||||
{:noreply, %{state | data: new_data}} |
||||
end |
||||
end |
@ -0,0 +1,787 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Utils.Db do |
||||
@moduledoc """ |
||||
Common functions to simplify DB routines for Indexer.Fetcher.Arbitrum fetchers |
||||
""" |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_warning: 1] |
||||
|
||||
alias Explorer.{Chain, Repo} |
||||
alias Explorer.Chain.Arbitrum.Reader |
||||
alias Explorer.Chain.Block, as: FullBlock |
||||
alias Explorer.Chain.{Data, Hash, Log} |
||||
|
||||
alias Explorer.Utility.MissingBlockRange |
||||
|
||||
require Logger |
||||
|
||||
# 32-byte signature of the event L2ToL1Tx(address caller, address indexed destination, uint256 indexed hash, uint256 indexed position, uint256 arbBlockNum, uint256 ethBlockNum, uint256 timestamp, uint256 callvalue, bytes data) |
||||
@l2_to_l1_event "0x3e7aafa77dbf186b7fd488006beff893744caa3c4f6f299e8a709fa2087374fc" |
||||
|
||||
@doc """ |
||||
Indexes L1 transactions provided in the input map. For transactions that |
||||
are already in the database, existing indices are taken. For new transactions, |
||||
the next available indices are assigned. |
||||
|
||||
## Parameters |
||||
- `new_l1_txs`: A map of L1 transaction descriptions. The keys of the map are |
||||
transaction hashes. |
||||
|
||||
## Returns |
||||
- `l1_txs`: A map of L1 transaction descriptions. Each element is extended with |
||||
the key `:id`, representing the index of the L1 transaction in the |
||||
`arbitrum_lifecycle_l1_transactions` table. |
||||
""" |
||||
@spec get_indices_for_l1_transactions(map()) :: map() |
||||
# TODO: consider a way to remove duplicate with ZkSync.Utils.Db |
||||
# credo:disable-for-next-line Credo.Check.Design.DuplicatedCode |
||||
def get_indices_for_l1_transactions(new_l1_txs) |
||||
when is_map(new_l1_txs) do |
||||
# Get indices for l1 transactions previously handled |
||||
l1_txs = |
||||
new_l1_txs |
||||
|> Map.keys() |
||||
|> Reader.lifecycle_transactions() |
||||
|> Enum.reduce(new_l1_txs, fn {hash, id}, txs -> |
||||
{_, txs} = |
||||
Map.get_and_update!(txs, hash.bytes, fn l1_tx -> |
||||
{l1_tx, Map.put(l1_tx, :id, id)} |
||||
end) |
||||
|
||||
txs |
||||
end) |
||||
|
||||
# Get the next index for the first new transaction based |
||||
# on the indices existing in DB |
||||
l1_tx_next_id = Reader.next_lifecycle_transaction_id() |
||||
|
||||
# Assign new indices for the transactions which are not in |
||||
# the l1 transactions table yet |
||||
{updated_l1_txs, _} = |
||||
l1_txs |
||||
|> Map.keys() |
||||
|> Enum.reduce( |
||||
{l1_txs, l1_tx_next_id}, |
||||
fn hash, {txs, next_id} -> |
||||
tx = txs[hash] |
||||
id = Map.get(tx, :id) |
||||
|
||||
if is_nil(id) do |
||||
{Map.put(txs, hash, Map.put(tx, :id, next_id)), next_id + 1} |
||||
else |
||||
{txs, next_id} |
||||
end |
||||
end |
||||
) |
||||
|
||||
updated_l1_txs |
||||
end |
||||
|
||||
@doc """ |
||||
Calculates the next L1 block number to search for the latest committed batch. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no committed batch is found. |
||||
|
||||
## Returns |
||||
- The next L1 block number after the latest committed batch or `value_if_nil` if no committed batches are found. |
||||
""" |
||||
@spec l1_block_to_discover_latest_committed_batch(FullBlock.block_number() | nil) :: FullBlock.block_number() | nil |
||||
def l1_block_to_discover_latest_committed_batch(value_if_nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.l1_block_of_latest_committed_batch() do |
||||
nil -> |
||||
log_warning("No committed batches found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value + 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Calculates the L1 block number to start the search for committed batches that precede |
||||
the earliest batch already discovered. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no committed batch is found. |
||||
|
||||
## Returns |
||||
- The L1 block number immediately preceding the earliest committed batch, |
||||
or `value_if_nil` if no committed batches are found. |
||||
""" |
||||
@spec l1_block_to_discover_earliest_committed_batch(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() |
||||
def l1_block_to_discover_earliest_committed_batch(value_if_nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.l1_block_of_earliest_committed_batch() do |
||||
nil -> |
||||
log_warning("No committed batches found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value - 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the block number of the highest rollup block that has been included in a batch. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no rollup batches are found. |
||||
|
||||
## Returns |
||||
- The number of the highest rollup block included in a batch |
||||
or `value_if_nil` if no rollup batches are found. |
||||
""" |
||||
@spec highest_committed_block(nil | integer()) :: nil | FullBlock.block_number() |
||||
def highest_committed_block(value_if_nil) |
||||
when is_integer(value_if_nil) or is_nil(value_if_nil) do |
||||
case Reader.highest_committed_block() do |
||||
nil -> value_if_nil |
||||
value -> value |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Calculates the next L1 block number to search for the latest message sent to L2. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no L1-to-L2 messages have been discovered. |
||||
|
||||
## Returns |
||||
- The L1 block number immediately following the latest discovered message to L2, |
||||
or `value_if_nil` if no messages to L2 have been found. |
||||
""" |
||||
@spec l1_block_to_discover_latest_message_to_l2(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() |
||||
def l1_block_to_discover_latest_message_to_l2(value_if_nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.l1_block_of_latest_discovered_message_to_l2() do |
||||
nil -> |
||||
log_warning("No messages to L2 found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value + 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Calculates the next L1 block number to start the search for messages sent to L2 |
||||
that precede the earliest message already discovered. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no L1-to-L2 messages have been discovered. |
||||
|
||||
## Returns |
||||
- The L1 block number immediately preceding the earliest discovered message to L2, |
||||
or `value_if_nil` if no messages to L2 have been found. |
||||
""" |
||||
@spec l1_block_to_discover_earliest_message_to_l2(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() |
||||
def l1_block_to_discover_earliest_message_to_l2(value_if_nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.l1_block_of_earliest_discovered_message_to_l2() do |
||||
nil -> |
||||
log_warning("No messages to L2 found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value - 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Determines the rollup block number to start searching for missed messages originating from L2. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no messages originating from L2 have been found. |
||||
|
||||
## Returns |
||||
- The rollup block number just before the earliest discovered message from L2, |
||||
or `value_if_nil` if no messages from L2 are found. |
||||
""" |
||||
@spec rollup_block_to_discover_missed_messages_from_l2(nil | FullBlock.block_number()) :: |
||||
nil | FullBlock.block_number() |
||||
def rollup_block_to_discover_missed_messages_from_l2(value_if_nil \\ nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.rollup_block_of_earliest_discovered_message_from_l2() do |
||||
nil -> |
||||
log_warning("No messages from L2 found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value - 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Determines the rollup block number to start searching for missed messages originating to L2. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no messages originating to L2 have been found. |
||||
|
||||
## Returns |
||||
- The rollup block number just before the earliest discovered message to L2, |
||||
or `value_if_nil` if no messages to L2 are found. |
||||
""" |
||||
@spec rollup_block_to_discover_missed_messages_to_l2(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() |
||||
def rollup_block_to_discover_missed_messages_to_l2(value_if_nil \\ nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.rollup_block_of_earliest_discovered_message_to_l2() do |
||||
nil -> |
||||
# In theory it could be a situation when when the earliest message points |
||||
# to a completion transaction which is not indexed yet. In this case, this |
||||
# warning will occur. |
||||
log_warning("No completed messages to L2 found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value - 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the L1 block number immediately following the block where the confirmation transaction |
||||
for the highest confirmed rollup block was included. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no confirmed rollup blocks are found. |
||||
|
||||
## Returns |
||||
- The L1 block number immediately after the block containing the confirmation transaction of |
||||
the highest confirmed rollup block, or `value_if_nil` if no confirmed rollup blocks are present. |
||||
""" |
||||
@spec l1_block_of_latest_confirmed_block(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() |
||||
def l1_block_of_latest_confirmed_block(value_if_nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.l1_block_of_latest_confirmed_block() do |
||||
nil -> |
||||
log_warning("No confirmed blocks found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value + 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the block number of the highest rollup block for which a confirmation transaction |
||||
has been sent to L1. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no confirmed rollup blocks are found. |
||||
|
||||
## Returns |
||||
- The block number of the highest confirmed rollup block, |
||||
or `value_if_nil` if no confirmed rollup blocks are found in the database. |
||||
""" |
||||
@spec highest_confirmed_block(nil | integer()) :: nil | FullBlock.block_number() |
||||
def highest_confirmed_block(value_if_nil) |
||||
when is_integer(value_if_nil) or is_nil(value_if_nil) do |
||||
case Reader.highest_confirmed_block() do |
||||
nil -> value_if_nil |
||||
value -> value |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Determines the next L1 block number to search for the latest execution of an L2-to-L1 message. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no execution transactions for L2-to-L1 messages |
||||
have been recorded. |
||||
|
||||
## Returns |
||||
- The L1 block number following the block that contains the latest execution transaction |
||||
for an L2-to-L1 message, or `value_if_nil` if no such executions have been found. |
||||
""" |
||||
@spec l1_block_to_discover_latest_execution(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() |
||||
def l1_block_to_discover_latest_execution(value_if_nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.l1_block_of_latest_execution() do |
||||
nil -> |
||||
log_warning("No L1 executions found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value + 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Determines the L1 block number just before the block that contains the earliest known |
||||
execution transaction for an L2-to-L1 message. |
||||
|
||||
## Parameters |
||||
- `value_if_nil`: The default value to return if no execution transactions for |
||||
L2-to-L1 messages have been found. |
||||
|
||||
## Returns |
||||
- The L1 block number preceding the earliest known execution transaction for |
||||
an L2-to-L1 message, or `value_if_nil` if no such executions are found in the database. |
||||
""" |
||||
@spec l1_block_to_discover_earliest_execution(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() |
||||
def l1_block_to_discover_earliest_execution(value_if_nil) |
||||
when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do |
||||
case Reader.l1_block_of_earliest_execution() do |
||||
nil -> |
||||
log_warning("No L1 executions found in DB") |
||||
value_if_nil |
||||
|
||||
value -> |
||||
value - 1 |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves full details of rollup blocks, including associated transactions, for each |
||||
block number specified in the input list. |
||||
|
||||
## Parameters |
||||
- `list_of_block_numbers`: A list of block numbers for which full block details are to be retrieved. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Block` instances containing detailed information for each |
||||
block number in the input list. Returns an empty list if no blocks are found for the given numbers. |
||||
""" |
||||
@spec rollup_blocks(maybe_improper_list(FullBlock.block_number(), [])) :: [FullBlock] |
||||
def rollup_blocks(list_of_block_numbers) |
||||
when is_list(list_of_block_numbers) do |
||||
query = |
||||
from( |
||||
block in FullBlock, |
||||
where: block.number in ^list_of_block_numbers |
||||
) |
||||
|
||||
query |
||||
# :optional is used since a block may not have any transactions |
||||
|> Chain.join_associations(%{:transactions => :optional}) |
||||
|> Repo.all(timeout: :infinity) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves unfinalized L1 transactions that are involved in changing the statuses |
||||
of rollup blocks or transactions. |
||||
|
||||
An L1 transaction is considered unfinalized if it has not yet reached a state |
||||
where it is permanently included in the blockchain, meaning it is still susceptible |
||||
to potential reorganization or change. Transactions are evaluated against |
||||
the finalized_block parameter to determine their finalized status. |
||||
|
||||
## Parameters |
||||
- `finalized_block`: The block number up to which unfinalized transactions are to be retrieved. |
||||
|
||||
## Returns |
||||
- A list of maps representing unfinalized L1 transactions and compatible with the |
||||
database import operation. |
||||
""" |
||||
@spec lifecycle_unfinalized_transactions(FullBlock.block_number()) :: [ |
||||
%{ |
||||
id: non_neg_integer(), |
||||
hash: Hash, |
||||
block_number: FullBlock.block_number(), |
||||
timestamp: DateTime, |
||||
status: :unfinalized |
||||
} |
||||
] |
||||
def lifecycle_unfinalized_transactions(finalized_block) |
||||
when is_integer(finalized_block) and finalized_block >= 0 do |
||||
finalized_block |
||||
|> Reader.lifecycle_unfinalized_transactions() |
||||
|> Enum.map(&lifecycle_transaction_to_map/1) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the block number associated with a specific hash of a rollup block. |
||||
|
||||
## Parameters |
||||
- `hash`: The hash of the rollup block whose number is to be retrieved. |
||||
|
||||
## Returns |
||||
- The block number associated with the given rollup block hash. |
||||
""" |
||||
@spec rollup_block_hash_to_num(binary()) :: FullBlock.block_number() | nil |
||||
def rollup_block_hash_to_num(hash) when is_binary(hash) do |
||||
Reader.rollup_block_hash_to_num(hash) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the L1 batch that includes a specified rollup block number. |
||||
|
||||
## Parameters |
||||
- `num`: The block number of the rollup block for which the containing |
||||
L1 batch is to be retrieved. |
||||
|
||||
## Returns |
||||
- The `Explorer.Chain.Arbitrum.L1Batch` associated with the given rollup block number |
||||
if it exists and its commit transaction is loaded. |
||||
""" |
||||
@spec get_batch_by_rollup_block_number(FullBlock.block_number()) :: Explorer.Chain.Arbitrum.L1Batch | nil |
||||
def get_batch_by_rollup_block_number(num) |
||||
when is_integer(num) and num >= 0 do |
||||
case Reader.get_batch_by_rollup_block_number(num) do |
||||
nil -> |
||||
nil |
||||
|
||||
batch -> |
||||
case batch.commitment_transaction do |
||||
nil -> |
||||
raise "Incorrect state of the DB: commitment_transaction is not loaded for the batch with number #{num}" |
||||
|
||||
%Ecto.Association.NotLoaded{} -> |
||||
raise "Incorrect state of the DB: commitment_transaction is not loaded for the batch with number #{num}" |
||||
|
||||
_ -> |
||||
batch |
||||
end |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves rollup blocks within a specified block range that have not yet been confirmed. |
||||
|
||||
## Parameters |
||||
- `first_block`: The starting block number of the range to search for unconfirmed rollup blocks. |
||||
- `last_block`: The ending block number of the range. |
||||
|
||||
## Returns |
||||
- A list of maps, each representing an unconfirmed rollup block within the specified range. |
||||
If no unconfirmed blocks are found within the range, an empty list is returned. |
||||
""" |
||||
@spec unconfirmed_rollup_blocks(FullBlock.block_number(), FullBlock.block_number()) :: [ |
||||
%{ |
||||
batch_number: non_neg_integer(), |
||||
block_number: FullBlock.block_number(), |
||||
confirmation_id: non_neg_integer() | nil |
||||
} |
||||
] |
||||
def unconfirmed_rollup_blocks(first_block, last_block) |
||||
when is_integer(first_block) and first_block >= 0 and |
||||
is_integer(last_block) and first_block <= last_block do |
||||
# credo:disable-for-lines:2 Credo.Check.Refactor.PipeChainStart |
||||
Reader.unconfirmed_rollup_blocks(first_block, last_block) |
||||
|> Enum.map(&rollup_block_to_map/1) |
||||
end |
||||
|
||||
@doc """ |
||||
Counts the number of confirmed rollup blocks in a specified batch. |
||||
|
||||
## Parameters |
||||
- `batch_number`: The batch number for which the count of confirmed rollup blocks |
||||
is to be determined. |
||||
|
||||
## Returns |
||||
- A number of rollup blocks confirmed in the specified batch. |
||||
""" |
||||
@spec count_confirmed_rollup_blocks_in_batch(non_neg_integer()) :: non_neg_integer() |
||||
def count_confirmed_rollup_blocks_in_batch(batch_number) |
||||
when is_integer(batch_number) and batch_number >= 0 do |
||||
Reader.count_confirmed_rollup_blocks_in_batch(batch_number) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a list of L2-to-L1 messages that have been initiated up to |
||||
a specified rollup block number. |
||||
|
||||
## Parameters |
||||
- `block_number`: The block number up to which initiated L2-to-L1 messages |
||||
should be retrieved. |
||||
|
||||
## Returns |
||||
- A list of maps, each representing an initiated L2-to-L1 message compatible with the |
||||
database import operation. If no initiated messages are found up to the specified |
||||
block number, an empty list is returned. |
||||
""" |
||||
@spec initiated_l2_to_l1_messages(FullBlock.block_number()) :: [ |
||||
%{ |
||||
direction: :from_l2, |
||||
message_id: non_neg_integer(), |
||||
originator_address: binary(), |
||||
originating_transaction_hash: binary(), |
||||
originating_transaction_block_number: FullBlock.block_number(), |
||||
completion_transaction_hash: nil, |
||||
status: :initiated |
||||
} |
||||
] |
||||
def initiated_l2_to_l1_messages(block_number) |
||||
when is_integer(block_number) and block_number >= 0 do |
||||
# credo:disable-for-lines:2 Credo.Check.Refactor.PipeChainStart |
||||
Reader.l2_to_l1_messages(:initiated, block_number) |
||||
|> Enum.map(&message_to_map/1) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a list of L2-to-L1 'sent' messages that have been included up to |
||||
a specified rollup block number. |
||||
|
||||
A message is considered 'sent' when there is a batch including the transaction |
||||
that initiated the message, and this batch has been successfully delivered to L1. |
||||
|
||||
## Parameters |
||||
- `block_number`: The block number up to which sent L2-to-L1 messages are to be retrieved. |
||||
|
||||
## Returns |
||||
- A list of maps, each representing a sent L2-to-L1 message compatible with the |
||||
database import operation. If no messages with the 'sent' status are found by |
||||
the specified block number, an empty list is returned. |
||||
""" |
||||
@spec sent_l2_to_l1_messages(FullBlock.block_number()) :: [ |
||||
%{ |
||||
direction: :from_l2, |
||||
message_id: non_neg_integer(), |
||||
originator_address: binary(), |
||||
originating_transaction_hash: binary(), |
||||
originating_transaction_block_number: FullBlock.block_number(), |
||||
completion_transaction_hash: nil, |
||||
status: :sent |
||||
} |
||||
] |
||||
def sent_l2_to_l1_messages(block_number) |
||||
when is_integer(block_number) and block_number >= 0 do |
||||
# credo:disable-for-lines:2 Credo.Check.Refactor.PipeChainStart |
||||
Reader.l2_to_l1_messages(:sent, block_number) |
||||
|> Enum.map(&message_to_map/1) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves a list of L2-to-L1 'confirmed' messages that have been included up to |
||||
a specified rollup block number. |
||||
|
||||
A message is considered 'confirmed' when its transaction was included in a rollup block, |
||||
and the confirmation of this block has been delivered to L1. |
||||
|
||||
## Parameters |
||||
- `block_number`: The block number up to which confirmed L2-to-L1 messages are to be retrieved. |
||||
|
||||
## Returns |
||||
- A list of maps, each representing a confirmed L2-to-L1 message compatible with the |
||||
database import operation. If no messages with the 'confirmed' status are found by |
||||
the specified block number, an empty list is returned. |
||||
""" |
||||
@spec confirmed_l2_to_l1_messages(FullBlock.block_number()) :: [ |
||||
%{ |
||||
direction: :from_l2, |
||||
message_id: non_neg_integer(), |
||||
originator_address: binary(), |
||||
originating_transaction_hash: binary(), |
||||
originating_transaction_block_number: FullBlock.block_number(), |
||||
completion_transaction_hash: nil, |
||||
status: :confirmed |
||||
} |
||||
] |
||||
def confirmed_l2_to_l1_messages(block_number) |
||||
when is_integer(block_number) and block_number >= 0 do |
||||
# credo:disable-for-lines:2 Credo.Check.Refactor.PipeChainStart |
||||
Reader.l2_to_l1_messages(:confirmed, block_number) |
||||
|> Enum.map(&message_to_map/1) |
||||
end |
||||
|
||||
@doc """ |
||||
Checks if the numbers from the provided list correspond to the numbers of indexed batches. |
||||
|
||||
## Parameters |
||||
- `batches_numbers`: The list of batch numbers. |
||||
|
||||
## Returns |
||||
- A list of batch numbers that are indexed and match the provided list, or `[]` |
||||
if none of the batch numbers in the provided list exist in the database. The output list |
||||
may be smaller than the input list. |
||||
""" |
||||
@spec batches_exist([non_neg_integer()]) :: [non_neg_integer()] |
||||
def batches_exist(batches_numbers) when is_list(batches_numbers) do |
||||
Reader.batches_exist(batches_numbers) |
||||
end |
||||
|
||||
@doc """ |
||||
Reads a list of transactions executing L2-to-L1 messages by their IDs. |
||||
|
||||
## Parameters |
||||
- `message_ids`: A list of IDs to retrieve executing transactions for. |
||||
|
||||
## Returns |
||||
- A list of `Explorer.Chain.Arbitrum.L1Execution` corresponding to the message IDs from |
||||
the input list. The output list may be smaller than the input list if some IDs do not |
||||
correspond to any existing transactions. |
||||
""" |
||||
@spec l1_executions([non_neg_integer()]) :: [Explorer.Chain.Arbitrum.L1Execution] |
||||
def l1_executions(message_ids) when is_list(message_ids) do |
||||
Reader.l1_executions(message_ids) |
||||
end |
||||
|
||||
@doc """ |
||||
Identifies the range of L1 blocks to investigate for missing confirmations of rollup blocks. |
||||
|
||||
This function determines the L1 block numbers bounding the interval where gaps in rollup block |
||||
confirmations might exist. It uses the earliest and latest L1 block numbers associated with |
||||
unconfirmed rollup blocks to define this range. |
||||
|
||||
## Parameters |
||||
- `right_pos_value_if_nil`: The default value to use for the upper bound of the range if no |
||||
confirmed blocks found. |
||||
|
||||
## Returns |
||||
- A tuple containing two elements: the lower and upper bounds of L1 block numbers to check |
||||
for missing rollup block confirmations. If the necessary confirmation data is unavailable, |
||||
the first element will be `nil`, and the second will be `right_pos_value_if_nil`. |
||||
""" |
||||
@spec l1_blocks_to_expect_rollup_blocks_confirmation(nil | FullBlock.block_number()) :: |
||||
{nil | FullBlock.block_number(), nil | FullBlock.block_number()} |
||||
def l1_blocks_to_expect_rollup_blocks_confirmation(right_pos_value_if_nil) |
||||
when (is_integer(right_pos_value_if_nil) and right_pos_value_if_nil >= 0) or is_nil(right_pos_value_if_nil) do |
||||
case Reader.l1_blocks_of_confirmations_bounding_first_unconfirmed_rollup_blocks_gap() do |
||||
nil -> |
||||
log_warning("No L1 confirmations found in DB") |
||||
{nil, right_pos_value_if_nil} |
||||
|
||||
{nil, newer_confirmation_l1_block} -> |
||||
{nil, newer_confirmation_l1_block - 1} |
||||
|
||||
{older_confirmation_l1_block, newer_confirmation_l1_block} -> |
||||
{older_confirmation_l1_block + 1, newer_confirmation_l1_block - 1} |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves all rollup logs in the range of blocks from `start_block` to `end_block` |
||||
corresponding to the `L2ToL1Tx` event emitted by the ArbSys contract. |
||||
|
||||
## Parameters |
||||
- `start_block`: The starting block number of the range from which to |
||||
retrieve the transaction logs containing L2-to-L1 messages. |
||||
- `end_block`: The ending block number of the range. |
||||
|
||||
## Returns |
||||
- A list of log maps for the `L2ToL1Tx` event where binary values for hashes |
||||
and data are decoded into hex strings, containing detailed information about |
||||
each event within the specified block range. Returns an empty list if no |
||||
relevant logs are found. |
||||
""" |
||||
@spec l2_to_l1_logs(FullBlock.block_number(), FullBlock.block_number()) :: [ |
||||
%{ |
||||
data: String, |
||||
index: non_neg_integer(), |
||||
first_topic: String, |
||||
second_topic: String, |
||||
third_topic: String, |
||||
fourth_topic: String, |
||||
address_hash: String, |
||||
transaction_hash: String, |
||||
block_hash: String, |
||||
block_number: FullBlock.block_number() |
||||
} |
||||
] |
||||
def l2_to_l1_logs(start_block, end_block) |
||||
when is_integer(start_block) and start_block >= 0 and |
||||
is_integer(end_block) and start_block <= end_block do |
||||
arbsys_contract = Application.get_env(:indexer, Indexer.Fetcher.Arbitrum.Messaging)[:arbsys_contract] |
||||
|
||||
query = |
||||
from(log in Log, |
||||
where: |
||||
log.block_number >= ^start_block and |
||||
log.block_number <= ^end_block and |
||||
log.address_hash == ^arbsys_contract and |
||||
log.first_topic == ^@l2_to_l1_event |
||||
) |
||||
|
||||
query |
||||
|> Repo.all(timeout: :infinity) |
||||
|> Enum.map(&logs_to_map/1) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns 32-byte signature of the event `L2ToL1Tx` |
||||
""" |
||||
@spec l2_to_l1_event() :: <<_::528>> |
||||
def l2_to_l1_event, do: @l2_to_l1_event |
||||
|
||||
@doc """ |
||||
Determines whether a given range of block numbers has been fully indexed without any missing blocks. |
||||
|
||||
## Parameters |
||||
- `start_block`: The starting block number of the range to check for completeness in indexing. |
||||
- `end_block`: The ending block number of the range. |
||||
|
||||
## Returns |
||||
- `true` if the entire range from `start_block` to `end_block` is indexed and contains no missing |
||||
blocks, indicating no intersection with missing block ranges; `false` otherwise. |
||||
""" |
||||
@spec indexed_blocks?(FullBlock.block_number(), FullBlock.block_number()) :: boolean() |
||||
def indexed_blocks?(start_block, end_block) |
||||
when is_integer(start_block) and start_block >= 0 and |
||||
is_integer(end_block) and start_block <= end_block do |
||||
is_nil(MissingBlockRange.intersects_with_range(start_block, end_block)) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the block number for the closest block immediately after a given timestamp. |
||||
|
||||
## Parameters |
||||
- `timestamp`: The `DateTime` timestamp for which the closest subsequent block number is sought. |
||||
|
||||
## Returns |
||||
- `{:ok, block_number}` where `block_number` is the number of the closest block that occurred |
||||
after the specified timestamp. |
||||
- `{:error, :not_found}` if no block is found after the specified timestamp. |
||||
""" |
||||
@spec closest_block_after_timestamp(DateTime.t()) :: {:error, :not_found} | {:ok, FullBlock.block_number()} |
||||
def closest_block_after_timestamp(timestamp) do |
||||
Chain.timestamp_to_block_number(timestamp, :after, false) |
||||
end |
||||
|
||||
defp lifecycle_transaction_to_map(tx) do |
||||
[:id, :hash, :block_number, :timestamp, :status] |
||||
|> db_record_to_map(tx) |
||||
end |
||||
|
||||
defp rollup_block_to_map(block) do |
||||
[:batch_number, :block_number, :confirmation_id] |
||||
|> db_record_to_map(block) |
||||
end |
||||
|
||||
defp message_to_map(message) do |
||||
[ |
||||
:direction, |
||||
:message_id, |
||||
:originator_address, |
||||
:originating_transaction_hash, |
||||
:originating_transaction_block_number, |
||||
:completion_transaction_hash, |
||||
:status |
||||
] |
||||
|> db_record_to_map(message) |
||||
end |
||||
|
||||
defp logs_to_map(log) do |
||||
[ |
||||
:data, |
||||
:index, |
||||
:first_topic, |
||||
:second_topic, |
||||
:third_topic, |
||||
:fourth_topic, |
||||
:address_hash, |
||||
:transaction_hash, |
||||
:block_hash, |
||||
:block_number |
||||
] |
||||
|> db_record_to_map(log, true) |
||||
end |
||||
|
||||
defp db_record_to_map(required_keys, record, encode \\ false) do |
||||
required_keys |
||||
|> Enum.reduce(%{}, fn key, record_as_map -> |
||||
raw_value = Map.get(record, key) |
||||
|
||||
# credo:disable-for-lines:5 Credo.Check.Refactor.Nesting |
||||
value = |
||||
case raw_value do |
||||
%Hash{} -> if(encode, do: Hash.to_string(raw_value), else: raw_value.bytes) |
||||
%Data{} -> if(encode, do: Data.to_string(raw_value), else: raw_value.bytes) |
||||
_ -> raw_value |
||||
end |
||||
|
||||
Map.put(record_as_map, key, value) |
||||
end) |
||||
end |
||||
end |
@ -0,0 +1,86 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Utils.Helper do |
||||
@moduledoc """ |
||||
Provides utility functions to support the handling of Arbitrum-specific data fetching and processing in the indexer. |
||||
""" |
||||
|
||||
@doc """ |
||||
Increases a base duration by an amount specified in a map, if present. |
||||
|
||||
This function takes a map that may contain a duration key and a current duration value. |
||||
If the map contains a duration, it is added to the current duration; otherwise, the |
||||
current duration is returned unchanged. |
||||
|
||||
## Parameters |
||||
- `data`: A map that may contain a `:duration` key with its value representing |
||||
the amount of time to add. |
||||
- `cur_duration`: The current duration value, to which the duration from the map |
||||
will be added if present. |
||||
|
||||
## Returns |
||||
- The increased duration. |
||||
""" |
||||
@spec increase_duration( |
||||
%{optional(:duration) => non_neg_integer(), optional(any()) => any()}, |
||||
non_neg_integer() |
||||
) :: non_neg_integer() |
||||
def increase_duration(data, cur_duration) |
||||
when is_map(data) and is_integer(cur_duration) and cur_duration >= 0 do |
||||
if Map.has_key?(data, :duration) do |
||||
data.duration + cur_duration |
||||
else |
||||
cur_duration |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Enriches lifecycle transaction entries with timestamps and status based on provided block information and finalization tracking. |
||||
|
||||
This function takes a map of lifecycle transactions and extends each entry with |
||||
a timestamp (extracted from a corresponding map of block numbers to timestamps) |
||||
and a status. The status is determined based on whether finalization tracking is enabled. |
||||
|
||||
## Parameters |
||||
- `lifecycle_txs`: A map where each key is a transaction identifier, and the value is |
||||
a map containing at least the block number (`:block`). |
||||
- `blocks_to_ts`: A map linking block numbers to their corresponding timestamps. |
||||
- `track_finalization?`: A boolean flag indicating whether to mark transactions |
||||
as unfinalized or finalized. |
||||
|
||||
## Returns |
||||
- An updated map of the same structure as `lifecycle_txs` but with each transaction extended to include: |
||||
- `timestamp`: The timestamp of the block in which the transaction is included. |
||||
- `status`: Either `:unfinalized` if `track_finalization?` is `true`, or `:finalized` otherwise. |
||||
""" |
||||
@spec extend_lifecycle_txs_with_ts_and_status( |
||||
%{binary() => %{:block => non_neg_integer(), optional(any()) => any()}}, |
||||
%{non_neg_integer() => DateTime.t()}, |
||||
boolean() |
||||
) :: %{ |
||||
binary() => %{ |
||||
:block => non_neg_integer(), |
||||
:timestamp => DateTime.t(), |
||||
:status => :unfinalized | :finalized, |
||||
optional(any()) => any() |
||||
} |
||||
} |
||||
def extend_lifecycle_txs_with_ts_and_status(lifecycle_txs, blocks_to_ts, track_finalization?) |
||||
when is_map(lifecycle_txs) and is_map(blocks_to_ts) and is_boolean(track_finalization?) do |
||||
lifecycle_txs |
||||
|> Map.keys() |
||||
|> Enum.reduce(%{}, fn tx_key, updated_txs -> |
||||
Map.put( |
||||
updated_txs, |
||||
tx_key, |
||||
Map.merge(lifecycle_txs[tx_key], %{ |
||||
timestamp: blocks_to_ts[lifecycle_txs[tx_key].block_number], |
||||
status: |
||||
if track_finalization? do |
||||
:unfinalized |
||||
else |
||||
:finalized |
||||
end |
||||
}) |
||||
) |
||||
end) |
||||
end |
||||
end |
@ -0,0 +1,162 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Utils.Logging do |
||||
@moduledoc """ |
||||
Common logging functions for Indexer.Fetcher.Arbitrum fetchers |
||||
""" |
||||
require Logger |
||||
|
||||
@doc """ |
||||
A helper function to log a message with debug severity. Uses `Logger.debug` facility. |
||||
|
||||
## Parameters |
||||
- `msg`: a message to log |
||||
|
||||
## Returns |
||||
`:ok` |
||||
""" |
||||
@spec log_debug(any()) :: :ok |
||||
def log_debug(msg) do |
||||
Logger.debug(msg) |
||||
end |
||||
|
||||
@doc """ |
||||
A helper function to log a message with warning severity. Uses `Logger.warning` facility. |
||||
|
||||
## Parameters |
||||
- `msg`: a message to log |
||||
|
||||
## Returns |
||||
`:ok` |
||||
""" |
||||
@spec log_warning(any()) :: :ok |
||||
def log_warning(msg) do |
||||
Logger.warning(msg) |
||||
end |
||||
|
||||
@doc """ |
||||
A helper function to log a message with info severity. Uses `Logger.info` facility. |
||||
|
||||
## Parameters |
||||
- `msg`: a message to log |
||||
|
||||
## Returns |
||||
`:ok` |
||||
""" |
||||
@spec log_info(any()) :: :ok |
||||
def log_info(msg) do |
||||
Logger.info(msg) |
||||
end |
||||
|
||||
@doc """ |
||||
A helper function to log a message with error severity. Uses `Logger.error` facility. |
||||
|
||||
## Parameters |
||||
- `msg`: a message to log |
||||
|
||||
## Returns |
||||
`:ok` |
||||
""" |
||||
@spec log_error(any()) :: :ok |
||||
def log_error(msg) do |
||||
Logger.error(msg) |
||||
end |
||||
|
||||
@doc """ |
||||
A helper function to log progress when handling data items in chunks. |
||||
|
||||
## Parameters |
||||
- `prefix`: A prefix for the logging message. |
||||
- `data_items_names`: A tuple with singular and plural of data items names |
||||
- `chunk`: A list of data items numbers in the current chunk. |
||||
- `current_progress`: The total number of data items handled up to this moment. |
||||
- `total`: The total number of data items across all chunks. |
||||
|
||||
## Returns |
||||
`:ok` |
||||
|
||||
## Examples: |
||||
- `log_details_chunk_handling("A message", {"batch", "batches"}, [1, 2, 3], 0, 10)` produces |
||||
`A message for batches 1..3. Progress 30%` |
||||
- `log_details_chunk_handling("A message", {"batch", "batches"}, [2], 1, 10)` produces |
||||
`A message for batch 2. Progress 20%` |
||||
- `log_details_chunk_handling("A message", {"block", "blocks"}, [35], 0, 1)` produces |
||||
`A message for block 35.` |
||||
- `log_details_chunk_handling("A message", {"block", "blocks"}, [45, 50, 51, 52, 60], 1, 1)` produces |
||||
`A message for blocks 45, 50..52, 60.` |
||||
""" |
||||
@spec log_details_chunk_handling(binary(), tuple(), list(), non_neg_integer(), non_neg_integer()) :: :ok |
||||
def log_details_chunk_handling(prefix, data_items_names, chunk, current_progress, total) |
||||
# credo:disable-for-previous-line Credo.Check.Refactor.CyclomaticComplexity |
||||
when is_binary(prefix) and is_tuple(data_items_names) and is_list(chunk) and |
||||
(is_integer(current_progress) and current_progress >= 0) and |
||||
(is_integer(total) and total > 0) do |
||||
chunk_length = length(chunk) |
||||
|
||||
progress = |
||||
case chunk_length == total do |
||||
true -> |
||||
"" |
||||
|
||||
false -> |
||||
percentage = |
||||
(current_progress + chunk_length) |
||||
|> Decimal.div(total) |
||||
|> Decimal.mult(100) |
||||
|> Decimal.round(2) |
||||
|> Decimal.to_string() |
||||
|
||||
" Progress: #{percentage}%" |
||||
end |
||||
|
||||
if chunk_length == 1 do |
||||
log_debug("#{prefix} for #{elem(data_items_names, 0)} ##{Enum.at(chunk, 0)}.") |
||||
else |
||||
log_debug( |
||||
"#{prefix} for #{elem(data_items_names, 1)} #{Enum.join(shorten_numbers_list(chunk), ", ")}.#{progress}" |
||||
) |
||||
end |
||||
end |
||||
|
||||
# Transform list of numbers to the list of string where consequent values |
||||
# are combined to be displayed as a range. |
||||
# |
||||
# ## Parameters |
||||
# - `msg`: a message to log |
||||
# |
||||
# ## Returns |
||||
# `shorten_list` - resulting list after folding |
||||
# |
||||
# ## Examples: |
||||
# [1, 2, 3] => ["1..3"] |
||||
# [1, 3] => ["1", "3"] |
||||
# [1, 2] => ["1..2"] |
||||
# [1, 3, 4, 5] => ["1", "3..5"] |
||||
defp shorten_numbers_list(numbers_list) do |
||||
{shorten_list, _, _} = |
||||
numbers_list |
||||
|> Enum.sort() |
||||
|> Enum.reduce({[], nil, nil}, fn number, {shorten_list, prev_range_start, prev_number} -> |
||||
shorten_numbers_list_impl(number, shorten_list, prev_range_start, prev_number) |
||||
end) |
||||
|> then(fn {shorten_list, prev_range_start, prev_number} -> |
||||
shorten_numbers_list_impl(prev_number, shorten_list, prev_range_start, prev_number) |
||||
end) |
||||
|
||||
Enum.reverse(shorten_list) |
||||
end |
||||
|
||||
defp shorten_numbers_list_impl(number, shorten_list, prev_range_start, prev_number) do |
||||
cond do |
||||
is_nil(prev_number) -> |
||||
{[], number, number} |
||||
|
||||
prev_number + 1 != number and prev_range_start == prev_number -> |
||||
{["#{prev_range_start}" | shorten_list], number, number} |
||||
|
||||
prev_number + 1 != number -> |
||||
{["#{prev_range_start}..#{prev_number}" | shorten_list], number, number} |
||||
|
||||
true -> |
||||
{shorten_list, prev_range_start, number} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,391 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do |
||||
@moduledoc """ |
||||
Common functions to simplify RPC routines for Indexer.Fetcher.Arbitrum fetchers |
||||
""" |
||||
|
||||
import EthereumJSONRPC, |
||||
only: [json_rpc: 2, quantity_to_integer: 1, timestamp_to_datetime: 1] |
||||
|
||||
alias EthereumJSONRPC.Transport |
||||
alias Indexer.Helper, as: IndexerHelper |
||||
|
||||
@zero_hash "0000000000000000000000000000000000000000000000000000000000000000" |
||||
@rpc_resend_attempts 20 |
||||
|
||||
@selector_outbox "ce11e6ab" |
||||
@selector_sequencer_inbox "ee35f327" |
||||
@selector_bridge "e78cea92" |
||||
@rollup_contract_abi [ |
||||
%{ |
||||
"inputs" => [], |
||||
"name" => "outbox", |
||||
"outputs" => [ |
||||
%{ |
||||
"internalType" => "address", |
||||
"name" => "", |
||||
"type" => "address" |
||||
} |
||||
], |
||||
"stateMutability" => "view", |
||||
"type" => "function" |
||||
}, |
||||
%{ |
||||
"inputs" => [], |
||||
"name" => "sequencerInbox", |
||||
"outputs" => [ |
||||
%{ |
||||
"internalType" => "address", |
||||
"name" => "", |
||||
"type" => "address" |
||||
} |
||||
], |
||||
"stateMutability" => "view", |
||||
"type" => "function" |
||||
}, |
||||
%{ |
||||
"inputs" => [], |
||||
"name" => "bridge", |
||||
"outputs" => [ |
||||
%{ |
||||
"internalType" => "address", |
||||
"name" => "", |
||||
"type" => "address" |
||||
} |
||||
], |
||||
"stateMutability" => "view", |
||||
"type" => "function" |
||||
} |
||||
] |
||||
|
||||
@doc """ |
||||
Constructs a JSON RPC request to retrieve a transaction by its hash. |
||||
|
||||
## Parameters |
||||
- `%{hash: tx_hash, id: id}`: A map containing the transaction hash (`tx_hash`) and |
||||
an identifier (`id`) for the request, which can be used later to establish |
||||
correspondence between requests and responses. |
||||
|
||||
## Returns |
||||
- A `Transport.request()` struct representing the JSON RPC request for fetching |
||||
the transaction details associated with the given hash. |
||||
""" |
||||
@spec transaction_by_hash_request(%{hash: EthereumJSONRPC.hash(), id: non_neg_integer()}) :: Transport.request() |
||||
def transaction_by_hash_request(%{id: id, hash: tx_hash}) |
||||
when is_binary(tx_hash) and is_integer(id) do |
||||
EthereumJSONRPC.request(%{id: id, method: "eth_getTransactionByHash", params: [tx_hash]}) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves specific contract addresses associated with Arbitrum rollup contract. |
||||
|
||||
This function fetches the addresses of the bridge, sequencer inbox, and outbox |
||||
contracts related to the specified Arbitrum rollup address. It invokes one of |
||||
the contract methods `bridge()`, `sequencerInbox()`, or `outbox()` based on |
||||
the `contracts_set` parameter to obtain the required information. |
||||
|
||||
## Parameters |
||||
- `rollup_address`: The address of the Arbitrum rollup contract from which |
||||
information is being retrieved. |
||||
- `contracts_set`: A symbol indicating the set of contracts to retrieve (`:bridge` |
||||
for the bridge contract, `:inbox_outbox` for the sequencer |
||||
inbox and outbox contracts). |
||||
- `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
|
||||
## Returns |
||||
- A map with keys corresponding to the contract types (`:bridge`, `:sequencer_inbox`, |
||||
`:outbox`) and values representing the contract addresses. |
||||
""" |
||||
@spec get_contracts_for_rollup( |
||||
EthereumJSONRPC.address(), |
||||
:bridge | :inbox_outbox, |
||||
EthereumJSONRPC.json_rpc_named_arguments() |
||||
) :: %{(:bridge | :sequencer_inbox | :outbox) => binary()} |
||||
def get_contracts_for_rollup(rollup_address, contracts_set, json_rpc_named_arguments) |
||||
|
||||
def get_contracts_for_rollup(rollup_address, :bridge, json_rpc_named_arguments) do |
||||
call_simple_getters_in_rollup_contract(rollup_address, [@selector_bridge], json_rpc_named_arguments) |
||||
end |
||||
|
||||
def get_contracts_for_rollup(rollup_address, :inbox_outbox, json_rpc_named_arguments) do |
||||
call_simple_getters_in_rollup_contract( |
||||
rollup_address, |
||||
[@selector_sequencer_inbox, @selector_outbox], |
||||
json_rpc_named_arguments |
||||
) |
||||
end |
||||
|
||||
# Calls getter functions on a rollup contract and collects their return values. |
||||
# |
||||
# This function is designed to interact with a rollup contract and invoke specified getter methods. |
||||
# It creates a list of requests for each method ID, executes these requests with retries as needed, |
||||
# and then maps the results to the corresponding method IDs. |
||||
# |
||||
# ## Parameters |
||||
# - `rollup_address`: The address of the rollup contract to interact with. |
||||
# - `method_ids`: A list of method identifiers representing the getter functions to be called. |
||||
# - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
# |
||||
# ## Returns |
||||
# - A map where each key is a method identifier converted to an atom, and each value is the |
||||
# response from calling the respective method on the contract. |
||||
defp call_simple_getters_in_rollup_contract(rollup_address, method_ids, json_rpc_named_arguments) do |
||||
method_ids |
||||
|> Enum.map(fn method_id -> |
||||
%{ |
||||
contract_address: rollup_address, |
||||
method_id: method_id, |
||||
args: [] |
||||
} |
||||
end) |
||||
|> IndexerHelper.read_contracts_with_retries(@rollup_contract_abi, json_rpc_named_arguments, @rpc_resend_attempts) |
||||
|> Kernel.elem(0) |
||||
|> Enum.zip(method_ids) |
||||
|> Enum.reduce(%{}, fn {{:ok, [response]}, method_id}, retval -> |
||||
Map.put(retval, atomized_key(method_id), response) |
||||
end) |
||||
end |
||||
|
||||
@doc """ |
||||
Executes a batch of RPC calls and returns a list of response bodies. |
||||
|
||||
This function processes a list of RPC requests and returns only the response bodies, |
||||
discarding the request IDs. The function is designed for scenarios where only |
||||
the response data is required, and the association with request IDs is not needed. |
||||
|
||||
## Parameters |
||||
- `requests_list`: A list of `Transport.request()` instances representing the RPC calls to be made. |
||||
- `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
- `help_str`: A string that helps identify the request type in log messages, used for error logging. |
||||
|
||||
## Returns |
||||
- A list containing the bodies of the RPC call responses. This list will include both |
||||
successful responses and errors encountered during the batch execution. The developer |
||||
must handle these outcomes as appropriate. |
||||
""" |
||||
@spec make_chunked_request([Transport.request()], EthereumJSONRPC.json_rpc_named_arguments(), binary()) :: list() |
||||
def make_chunked_request(requests_list, json_rpc_named_arguments, help_str) |
||||
|
||||
def make_chunked_request([], _, _) do |
||||
[] |
||||
end |
||||
|
||||
def make_chunked_request(requests_list, json_rpc_named_arguments, help_str) |
||||
when is_list(requests_list) and is_binary(help_str) do |
||||
requests_list |
||||
|> make_chunked_request_keep_id(json_rpc_named_arguments, help_str) |
||||
|> Enum.map(fn %{result: resp_body} -> resp_body end) |
||||
end |
||||
|
||||
@doc """ |
||||
Executes a batch of RPC calls while preserving the original request IDs in the responses. |
||||
|
||||
This function processes a list of RPC requests in batches, retaining the association |
||||
between the requests and their responses to ensure that each response can be traced |
||||
back to its corresponding request. |
||||
|
||||
## Parameters |
||||
- `requests_list`: A list of `Transport.request()` instances representing the RPC calls to be made. |
||||
- `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
- `help_str`: A string that helps identify the request type in log messages, used for error logging. |
||||
|
||||
## Returns |
||||
- A list of maps, each containing the `id` and `result` from the RPC response, maintaining |
||||
the same order and ID as the original request. If the batch execution encounters errors |
||||
that cannot be resolved after the defined number of retries, the function will log |
||||
the errors using the provided `help_str` for context and will return a list of responses |
||||
where each element is either the result of a successful call or an error description. |
||||
It is the responsibility of the developer to distinguish between successful responses |
||||
and errors and handle them appropriately. |
||||
""" |
||||
@spec make_chunked_request_keep_id([Transport.request()], EthereumJSONRPC.json_rpc_named_arguments(), binary()) :: |
||||
[%{id: non_neg_integer(), result: any()}] |
||||
def make_chunked_request_keep_id(requests_list, json_rpc_named_arguments, help_str) |
||||
|
||||
def make_chunked_request_keep_id([], _, _) do |
||||
[] |
||||
end |
||||
|
||||
def make_chunked_request_keep_id(requests_list, json_rpc_named_arguments, help_str) |
||||
when is_list(requests_list) and is_binary(help_str) do |
||||
error_message_generator = &"Cannot call #{help_str}. Error: #{inspect(&1)}" |
||||
|
||||
{:ok, responses} = |
||||
IndexerHelper.repeated_batch_rpc_call( |
||||
requests_list, |
||||
json_rpc_named_arguments, |
||||
error_message_generator, |
||||
@rpc_resend_attempts |
||||
) |
||||
|
||||
responses |
||||
end |
||||
|
||||
@doc """ |
||||
Executes a list of block requests, retrieves their timestamps, and returns a map of block numbers to timestamps. |
||||
|
||||
## Parameters |
||||
- `blocks_requests`: A list of `Transport.request()` instances representing the block |
||||
information requests. |
||||
- `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
- `chunk_size`: The number of requests to be processed in each batch, defining the size of the chunks. |
||||
|
||||
## Returns |
||||
- A map where each key is a block number and each value is the corresponding timestamp. |
||||
""" |
||||
@spec execute_blocks_requests_and_get_ts( |
||||
[Transport.request()], |
||||
EthereumJSONRPC.json_rpc_named_arguments(), |
||||
non_neg_integer() |
||||
) :: %{EthereumJSONRPC.block_number() => DateTime.t()} |
||||
def execute_blocks_requests_and_get_ts(blocks_requests, json_rpc_named_arguments, chunk_size) |
||||
when is_list(blocks_requests) and is_integer(chunk_size) do |
||||
blocks_requests |
||||
|> Enum.chunk_every(chunk_size) |
||||
|> Enum.reduce(%{}, fn chunk, result -> |
||||
chunk |
||||
|> make_chunked_request(json_rpc_named_arguments, "eth_getBlockByNumber") |
||||
|> Enum.reduce(result, fn resp, result_inner -> |
||||
Map.put(result_inner, quantity_to_integer(resp["number"]), timestamp_to_datetime(resp["timestamp"])) |
||||
end) |
||||
end) |
||||
end |
||||
|
||||
@doc """ |
||||
Executes a list of transaction requests and retrieves the sender (from) addresses for each. |
||||
|
||||
## Parameters |
||||
- `txs_requests`: A list of `Transport.request()` instances representing the transaction requests. |
||||
- `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
- `chunk_size`: The number of requests to be processed in each batch, defining the size of the chunks. |
||||
|
||||
## Returns |
||||
- A map where each key is a transaction hash and each value is the corresponding sender's address. |
||||
""" |
||||
@spec execute_transactions_requests_and_get_from( |
||||
[Transport.request()], |
||||
EthereumJSONRPC.json_rpc_named_arguments(), |
||||
non_neg_integer() |
||||
) :: [%{EthereumJSONRPC.hash() => EthereumJSONRPC.address()}] |
||||
def execute_transactions_requests_and_get_from(txs_requests, json_rpc_named_arguments, chunk_size) |
||||
when is_list(txs_requests) and is_integer(chunk_size) do |
||||
txs_requests |
||||
|> Enum.chunk_every(chunk_size) |
||||
|> Enum.reduce(%{}, fn chunk, result -> |
||||
chunk |
||||
|> make_chunked_request(json_rpc_named_arguments, "eth_getTransactionByHash") |
||||
|> Enum.reduce(result, fn resp, result_inner -> |
||||
Map.put(result_inner, resp["hash"], resp["from"]) |
||||
end) |
||||
end) |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the block number associated with a given block hash using the Ethereum JSON RPC `eth_getBlockByHash` method, with retry logic for handling request failures. |
||||
|
||||
## Parameters |
||||
- `hash`: The hash of the block for which the block number is requested. |
||||
- `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
|
||||
## Returns |
||||
- The block number if the block is found and successfully retrieved, or `nil` |
||||
if the block cannot be fetched or the block number is not present in the response. |
||||
""" |
||||
@spec get_block_number_by_hash(EthereumJSONRPC.hash(), EthereumJSONRPC.json_rpc_named_arguments()) :: |
||||
EthereumJSONRPC.block_number() | nil |
||||
def get_block_number_by_hash(hash, json_rpc_named_arguments) do |
||||
func = &do_get_block_number_by_hash/2 |
||||
args = [hash, json_rpc_named_arguments] |
||||
error_message = &"Cannot fetch block #{hash} or its number. Error: #{inspect(&1)}" |
||||
|
||||
case IndexerHelper.repeated_call(func, args, error_message, @rpc_resend_attempts) do |
||||
{:error, _} -> nil |
||||
{:ok, res} -> res |
||||
end |
||||
end |
||||
|
||||
defp do_get_block_number_by_hash(hash, json_rpc_named_arguments) do |
||||
# credo:disable-for-lines:3 Credo.Check.Refactor.PipeChainStart |
||||
result = |
||||
EthereumJSONRPC.request(%{id: 0, method: "eth_getBlockByHash", params: [hash, false]}) |
||||
|> json_rpc(json_rpc_named_arguments) |
||||
|
||||
with {:ok, block} <- result, |
||||
false <- is_nil(block), |
||||
number <- Map.get(block, "number"), |
||||
false <- is_nil(number) do |
||||
{:ok, quantity_to_integer(number)} |
||||
else |
||||
{:error, message} -> |
||||
{:error, message} |
||||
|
||||
true -> |
||||
{:error, "RPC returned nil."} |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Determines the starting block number for further operations with L1 based on configuration and network status. |
||||
|
||||
This function selects the starting block number for operations involving L1. |
||||
If the configured block number is `0`, it attempts to retrieve the safe block number |
||||
from the network. Should the safe block number not be available (if the endpoint does |
||||
not support this feature), the latest block number is used instead. If a non-zero block |
||||
number is configured, that number is used directly. |
||||
|
||||
## Parameters |
||||
- `configured_number`: The block number configured for starting operations. |
||||
- `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
|
||||
## Returns |
||||
- The block number from which to start further operations with L1, determined based |
||||
on the provided configuration and network capabilities. |
||||
""" |
||||
@spec get_l1_start_block(EthereumJSONRPC.block_number(), EthereumJSONRPC.json_rpc_named_arguments()) :: |
||||
EthereumJSONRPC.block_number() |
||||
def get_l1_start_block(configured_number, json_rpc_named_arguments) do |
||||
if configured_number == 0 do |
||||
{block_number, _} = IndexerHelper.get_safe_block(json_rpc_named_arguments) |
||||
block_number |
||||
else |
||||
configured_number |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Converts a transaction hash from its hexadecimal string representation to a binary format. |
||||
|
||||
## Parameters |
||||
- `hash`: The transaction hash as a hex string, which can be `nil`. If `nil`, a default zero hash value is used. |
||||
|
||||
## Returns |
||||
- The binary representation of the hash. If the input is `nil`, returns the binary form of the default zero hash. |
||||
""" |
||||
@spec string_hash_to_bytes_hash(EthereumJSONRPC.hash() | nil) :: binary() |
||||
def string_hash_to_bytes_hash(hash) do |
||||
hash |
||||
|> json_tx_id_to_hash() |
||||
|> Base.decode16!(case: :mixed) |
||||
end |
||||
|
||||
defp json_tx_id_to_hash(hash) do |
||||
case hash do |
||||
"0x" <> tx_hash -> tx_hash |
||||
nil -> @zero_hash |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves the hardcoded number of resend attempts for RPC calls. |
||||
|
||||
## Returns |
||||
- The number of resend attempts. |
||||
""" |
||||
@spec get_resend_attempts() :: non_neg_integer() |
||||
def get_resend_attempts do |
||||
@rpc_resend_attempts |
||||
end |
||||
|
||||
defp atomized_key(@selector_outbox), do: :outbox |
||||
defp atomized_key(@selector_sequencer_inbox), do: :sequencer_inbox |
||||
defp atomized_key(@selector_bridge), do: :bridge |
||||
end |
@ -0,0 +1,284 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Workers.HistoricalMessagesOnL2 do |
||||
@moduledoc """ |
||||
Handles the discovery and processing of historical messages between Layer 1 (L1) and Layer 2 (L2) within an Arbitrum rollup. |
||||
|
||||
L1-to-L2 messages are discovered by requesting rollup transactions through RPC. |
||||
This is necessary because some Arbitrum-specific fields are not included in the |
||||
already indexed transactions within the database. |
||||
|
||||
L2-to-L1 messages are discovered by analyzing the logs of already indexed rollup |
||||
transactions. |
||||
""" |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_warning: 1, log_info: 1] |
||||
|
||||
alias EthereumJSONRPC.Block.ByNumber, as: BlockByNumber |
||||
alias EthereumJSONRPC.Transaction, as: TransactionByRPC |
||||
|
||||
alias Explorer.Chain |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Messaging |
||||
alias Indexer.Fetcher.Arbitrum.Utils.{Db, Logging, Rpc} |
||||
|
||||
require Logger |
||||
|
||||
@doc """ |
||||
Initiates the discovery process for historical messages sent from L2 to L1 up to a specified block number. |
||||
|
||||
This function orchestrates the discovery of historical messages from L2 to L1 |
||||
by analyzing the rollup logs representing the `L2ToL1Tx` event. It determines |
||||
the starting block for the discovery process and verifies that the relevant |
||||
rollup block range has been indexed before proceeding with the discovery and |
||||
data import. During the import process, each message is assigned the |
||||
appropriate status based on the current rollup state. |
||||
|
||||
## Parameters |
||||
- `end_block`: The ending block number up to which the discovery should occur. |
||||
If `nil` or negative, the function returns with no action taken. |
||||
- `state`: Contains the operational configuration, including the depth of |
||||
blocks to consider for the starting point of message discovery. |
||||
|
||||
## Returns |
||||
- `{:ok, nil}`: If `end_block` is `nil`, indicating no discovery action was required. |
||||
- `{:ok, 0}`: If `end_block` is negative, indicating that the genesis of the block |
||||
chain was reached. |
||||
- `{:ok, start_block}`: Upon successful discovery of historical messages, where |
||||
`start_block` indicates the necessity to consider another |
||||
block range in the next iteration of message discovery. |
||||
- `{:ok, end_block + 1}`: If the required block range is not fully indexed, |
||||
indicating that the next iteration of message discovery |
||||
should start with the same block range. |
||||
""" |
||||
@spec discover_historical_messages_from_l2(nil | integer(), %{ |
||||
:config => %{ |
||||
:messages_to_l2_blocks_depth => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, nil | non_neg_integer()} |
||||
def discover_historical_messages_from_l2(end_block, state) |
||||
|
||||
def discover_historical_messages_from_l2(end_block, _) when is_nil(end_block) do |
||||
{:ok, nil} |
||||
end |
||||
|
||||
def discover_historical_messages_from_l2(end_block, _) |
||||
when is_integer(end_block) and end_block < 0 do |
||||
{:ok, 0} |
||||
end |
||||
|
||||
def discover_historical_messages_from_l2( |
||||
end_block, |
||||
%{config: %{messages_from_l2_blocks_depth: messages_from_l2_blocks_depth}} = _state |
||||
) |
||||
when is_integer(end_block) and is_integer(messages_from_l2_blocks_depth) and |
||||
messages_from_l2_blocks_depth > 0 do |
||||
start_block = max(0, end_block - messages_from_l2_blocks_depth + 1) |
||||
|
||||
if Db.indexed_blocks?(start_block, end_block) do |
||||
do_discover_historical_messages_from_l2(start_block, end_block) |
||||
else |
||||
log_warning( |
||||
"Not able to discover historical messages from L2, some blocks in #{start_block}..#{end_block} not indexed" |
||||
) |
||||
|
||||
{:ok, end_block + 1} |
||||
end |
||||
end |
||||
|
||||
# Discovers and processes historical messages sent from L2 to L1 within a specified rollup block range. |
||||
# |
||||
# This function fetches relevant rollup logs from the database representing messages sent |
||||
# from L2 to L1 (the `L2ToL1Tx` event) between the specified `start_block` and `end_block`. |
||||
# If any logs are found, they are used to construct message structures, which are then |
||||
# imported into the database. As part of the message construction, the appropriate status |
||||
# of the message (initialized, sent, or confirmed) is determined based on the current rollup |
||||
# state. |
||||
# |
||||
# ## Parameters |
||||
# - `start_block`: The starting block number for the discovery range. |
||||
# - `end_block`: The ending block number for the discovery range. |
||||
# |
||||
# ## Returns |
||||
# - `{:ok, start_block}`: A tuple indicating successful processing, returning the initial |
||||
# starting block number. |
||||
defp do_discover_historical_messages_from_l2(start_block, end_block) do |
||||
log_info("Block range for discovery historical messages from L2: #{start_block}..#{end_block}") |
||||
|
||||
logs = Db.l2_to_l1_logs(start_block, end_block) |
||||
|
||||
unless logs == [] do |
||||
messages = |
||||
logs |
||||
|> Messaging.handle_filtered_l2_to_l1_messages(__MODULE__) |
||||
|
||||
import_to_db(messages) |
||||
end |
||||
|
||||
{:ok, start_block} |
||||
end |
||||
|
||||
@doc """ |
||||
Initiates the discovery of historical messages sent from L1 to L2 up to a specified block number. |
||||
|
||||
This function orchestrates the process of discovering historical L1-to-L2 messages within |
||||
a given rollup block range, based on the existence of the `requestId` field in the rollup |
||||
transaction body. Transactions are requested through RPC because already indexed |
||||
transactions from the database cannot be utilized; the `requestId` field is not included |
||||
in the transaction model. The function ensures that the block range has been indexed |
||||
before proceeding with message discovery and import. The imported messages are marked as |
||||
`:relayed`, as they represent completed actions from L1 to L2. |
||||
|
||||
## Parameters |
||||
- `end_block`: The ending block number for the discovery operation. If `nil` or negative, |
||||
the function returns immediately with no action. |
||||
- `state`: The current state of the operation, containing configuration parameters |
||||
including `messages_to_l2_blocks_depth`, `chunk_size`, and JSON RPC connection settings. |
||||
|
||||
## Returns |
||||
- `{:ok, nil}`: If `end_block` is `nil`, indicating no action was necessary. |
||||
- `{:ok, 0}`: If `end_block` is negative, indicating that the genesis of the block chain |
||||
was reached. |
||||
- `{:ok, start_block}`: On successful completion of historical message discovery, where |
||||
`start_block` indicates the necessity to consider another block |
||||
range in the next iteration of message discovery. |
||||
- `{:ok, end_block + 1}`: If the required block range is not fully indexed, indicating |
||||
that the next iteration of message discovery should start with |
||||
the same block range. |
||||
""" |
||||
@spec discover_historical_messages_to_l2(nil | integer(), %{ |
||||
:config => %{ |
||||
:messages_to_l2_blocks_depth => non_neg_integer(), |
||||
:rollup_rpc => %{ |
||||
:chunk_size => non_neg_integer(), |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, nil | non_neg_integer()} |
||||
def discover_historical_messages_to_l2(end_block, state) |
||||
|
||||
def discover_historical_messages_to_l2(end_block, _) when is_nil(end_block) do |
||||
{:ok, nil} |
||||
end |
||||
|
||||
def discover_historical_messages_to_l2(end_block, _) |
||||
when is_integer(end_block) and end_block < 0 do |
||||
{:ok, 0} |
||||
end |
||||
|
||||
def discover_historical_messages_to_l2(end_block, %{config: %{messages_to_l2_blocks_depth: _} = config} = _state) |
||||
when is_integer(end_block) do |
||||
start_block = max(0, end_block - config.messages_to_l2_blocks_depth + 1) |
||||
|
||||
# Although indexing blocks is not necessary to determine the completion of L1-to-L2 messages, |
||||
# for database consistency, it is preferable to delay marking these messages as completed. |
||||
if Db.indexed_blocks?(start_block, end_block) do |
||||
do_discover_historical_messages_to_l2(start_block, end_block, config) |
||||
else |
||||
log_warning( |
||||
"Not able to discover historical messages to L2, some blocks in #{start_block}..#{end_block} not indexed" |
||||
) |
||||
|
||||
{:ok, end_block + 1} |
||||
end |
||||
end |
||||
|
||||
# The function iterates through the block range in chunks, making RPC calls to fetch rollup block |
||||
# data and extract transactions. Each transaction is filtered for L1-to-L2 messages based on |
||||
# existence of `requestId` field in the transaction body, and then imported into the database. |
||||
# The imported messages are marked as `:relayed` as they represent completed actions from L1 to L2. |
||||
# |
||||
# Already indexed transactions from the database cannot be used because the `requestId` field is |
||||
# not included in the transaction model. |
||||
# |
||||
# ## Parameters |
||||
# - `start_block`: The starting block number for the discovery range. |
||||
# - `end_block`: The ending block number for the discovery range. |
||||
# - `config`: The configuration map containing settings for RPC communication and chunk size. |
||||
# |
||||
# ## Returns |
||||
# - `{:ok, start_block}`: A tuple indicating successful processing, returning the initial |
||||
# starting block number. |
||||
defp do_discover_historical_messages_to_l2( |
||||
start_block, |
||||
end_block, |
||||
%{rollup_rpc: %{chunk_size: chunk_size, json_rpc_named_arguments: json_rpc_named_arguments}} = _config |
||||
) do |
||||
log_info("Block range for discovery historical messages to L2: #{start_block}..#{end_block}") |
||||
|
||||
{messages, _} = |
||||
start_block..end_block |
||||
|> Enum.chunk_every(chunk_size) |
||||
|> Enum.reduce({[], 0}, fn chunk, {messages_acc, chunks_counter} -> |
||||
Logging.log_details_chunk_handling( |
||||
"Collecting rollup data", |
||||
{"block", "blocks"}, |
||||
chunk, |
||||
chunks_counter, |
||||
end_block - start_block + 1 |
||||
) |
||||
|
||||
# Since DB does not contain the field RequestId specific to Arbitrum |
||||
# all transactions will be requested from the rollup RPC endpoint. |
||||
# The catchup process intended to be run once and only for the BS instance |
||||
# which are already exist, so it does not make sense to introduce |
||||
# the new field in DB |
||||
requests = build_block_by_number_requests(chunk) |
||||
|
||||
messages = |
||||
requests |
||||
|> Rpc.make_chunked_request(json_rpc_named_arguments, "eth_getBlockByNumber") |
||||
|> get_transactions() |
||||
|> Enum.map(fn tx -> |
||||
tx |
||||
|> TransactionByRPC.to_elixir() |
||||
|> TransactionByRPC.elixir_to_params() |
||||
end) |
||||
|> Messaging.filter_l1_to_l2_messages(false) |
||||
|
||||
{messages ++ messages_acc, chunks_counter + length(chunk)} |
||||
end) |
||||
|
||||
unless messages == [] do |
||||
log_info("#{length(messages)} completions of L1-to-L2 messages will be imported") |
||||
end |
||||
|
||||
import_to_db(messages) |
||||
|
||||
{:ok, start_block} |
||||
end |
||||
|
||||
# Constructs a list of `eth_getBlockByNumber` requests for a given list of block numbers. |
||||
defp build_block_by_number_requests(block_numbers) do |
||||
block_numbers |
||||
|> Enum.reduce([], fn block_num, requests_list -> |
||||
[ |
||||
BlockByNumber.request(%{ |
||||
id: block_num, |
||||
number: block_num |
||||
}) |
||||
| requests_list |
||||
] |
||||
end) |
||||
end |
||||
|
||||
# Aggregates transactions from a list of blocks, combining them into a single list. |
||||
defp get_transactions(blocks_by_rpc) do |
||||
blocks_by_rpc |
||||
|> Enum.reduce([], fn block_by_rpc, txs -> |
||||
block_by_rpc["transactions"] ++ txs |
||||
end) |
||||
end |
||||
|
||||
# Imports a list of messages into the database. |
||||
defp import_to_db(messages) do |
||||
{:ok, _} = |
||||
Chain.import(%{ |
||||
arbitrum_messages: %{params: messages}, |
||||
timeout: :infinity |
||||
}) |
||||
end |
||||
end |
@ -0,0 +1,74 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Workers.L1Finalization do |
||||
@moduledoc """ |
||||
Oversees the finalization of lifecycle transactions on Layer 1 (L1) for Arbitrum rollups. |
||||
|
||||
This module is tasked with monitoring and updating the status of Arbitrum |
||||
lifecycle transactions that are related to the rollup process. It ensures that |
||||
transactions which have been confirmed up to the 'safe' block number on L1 are |
||||
marked as 'finalized' within the system's database. |
||||
""" |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1] |
||||
|
||||
alias Indexer.Helper, as: IndexerHelper |
||||
alias Indexer.Fetcher.Arbitrum.Utils.{Db, Rpc} |
||||
|
||||
alias Explorer.Chain |
||||
|
||||
require Logger |
||||
|
||||
@doc """ |
||||
Monitors and updates the status of lifecycle transactions related an Arbitrum rollup to 'finalized'. |
||||
|
||||
This function retrieves the current 'safe' block number from L1 and identifies |
||||
lifecycle transactions that are not yet finalized up to this block. It then |
||||
updates the status of these transactions to 'finalized' and imports the updated |
||||
data into the database. |
||||
|
||||
## Parameters |
||||
- A map containing: |
||||
- `config`: Configuration settings including JSON RPC arguments for L1 used |
||||
to fetch the 'safe' block number. |
||||
|
||||
## Returns |
||||
- `:ok` |
||||
""" |
||||
@spec monitor_lifecycle_txs(%{ |
||||
:config => %{ |
||||
:l1_rpc => %{ |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}) :: :ok |
||||
def monitor_lifecycle_txs(%{config: %{l1_rpc: %{json_rpc_named_arguments: json_rpc_named_arguments}}} = _state) do |
||||
{:ok, safe_block} = |
||||
IndexerHelper.get_block_number_by_tag( |
||||
"safe", |
||||
json_rpc_named_arguments, |
||||
Rpc.get_resend_attempts() |
||||
) |
||||
|
||||
lifecycle_txs = Db.lifecycle_unfinalized_transactions(safe_block) |
||||
|
||||
if length(lifecycle_txs) > 0 do |
||||
log_info("Discovered #{length(lifecycle_txs)} lifecycle transaction to be finalized") |
||||
|
||||
updated_lifecycle_txs = |
||||
lifecycle_txs |
||||
|> Enum.map(fn tx -> |
||||
Map.put(tx, :status, :finalized) |
||||
end) |
||||
|
||||
{:ok, _} = |
||||
Chain.import(%{ |
||||
arbitrum_lifecycle_transactions: %{params: updated_lifecycle_txs}, |
||||
timeout: :infinity |
||||
}) |
||||
end |
||||
|
||||
:ok |
||||
end |
||||
end |
@ -0,0 +1,975 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Workers.NewBatches do |
||||
@moduledoc """ |
||||
Manages the discovery and importation of new and historical batches of transactions for an Arbitrum rollup. |
||||
|
||||
This module orchestrates the discovery of batches of transactions processed |
||||
through the Arbitrum Sequencer. It distinguishes between new batches currently |
||||
being created and historical batches processed in the past but not yet imported |
||||
into the database. |
||||
|
||||
The process involves fetching logs for the `SequencerBatchDelivered` event |
||||
emitted by the Arbitrum `SequencerInbox` contract, processing these logs to |
||||
extract batch details, and then building the link between batches and the |
||||
corresponding rollup blocks and transactions. It also discovers those |
||||
cross-chain messages initiated in rollup blocks linked with the new batches |
||||
and updates the status of messages to consider them as committed (`:sent`). |
||||
|
||||
For any blocks or transactions missing in the database, data is requested in |
||||
chunks from the rollup RPC endpoint by `eth_getBlockByNumber`. Additionally, |
||||
to complete batch details and lifecycle transactions, RPC calls to |
||||
`eth_getTransactionByHash` and `eth_getBlockByNumber` on L1 are made in chunks |
||||
for the necessary information not available in the logs. |
||||
""" |
||||
|
||||
alias ABI.{FunctionSelector, TypeDecoder} |
||||
|
||||
import EthereumJSONRPC, only: [quantity_to_integer: 1] |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1, log_debug: 1] |
||||
|
||||
alias EthereumJSONRPC.Block.ByNumber, as: BlockByNumber |
||||
|
||||
alias Indexer.Helper, as: IndexerHelper |
||||
alias Indexer.Fetcher.Arbitrum.Utils.{Db, Logging, Rpc} |
||||
|
||||
alias Explorer.Chain |
||||
|
||||
require Logger |
||||
|
||||
# keccak256("SequencerBatchDelivered(uint256,bytes32,bytes32,bytes32,uint256,(uint64,uint64,uint64,uint64),uint8)") |
||||
@message_sequencer_batch_delivered "0x7394f4a19a13c7b92b5bb71033245305946ef78452f7b4986ac1390b5df4ebd7" |
||||
|
||||
@doc """ |
||||
Discovers and imports new batches of rollup transactions within the current L1 block range. |
||||
|
||||
This function determines the L1 block range for discovering new batches of rollup |
||||
transactions. It retrieves logs representing SequencerBatchDelivered events |
||||
emitted by the SequencerInbox contract within this range. The logs are processed |
||||
to identify new batches and their corresponding details. Comprehensive data |
||||
structures for these batches, along with their lifecycle transactions, rollup |
||||
blocks, and rollup transactions, are constructed. In addition, the function |
||||
updates the status of L2-to-L1 messages that have been committed within these new |
||||
batches. All discovered and processed data are then imported into the database. |
||||
The process targets only the batches that have not been previously processed, |
||||
thereby enhancing efficiency. |
||||
|
||||
## Parameters |
||||
- A map containing: |
||||
- `config`: Configuration settings including RPC configurations, SequencerInbox |
||||
address, a shift for the message to block number mapping, and |
||||
a limit for new batches discovery. |
||||
- `data`: Contains the starting block number for new batch discovery. |
||||
|
||||
## Returns |
||||
- `{:ok, end_block}`: On successful discovery and processing, where `end_block` |
||||
indicates the necessity to consider the next block range |
||||
in the following iteration of new batch discovery. |
||||
- `{:ok, start_block - 1}`: If there are no new blocks to be processed, |
||||
indicating that the current start block should be |
||||
reconsidered in the next iteration. |
||||
""" |
||||
@spec discover_new_batches(%{ |
||||
:config => %{ |
||||
:l1_rpc => %{ |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:logs_block_range => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
:l1_sequencer_inbox_address => binary(), |
||||
:messages_to_blocks_shift => non_neg_integer(), |
||||
:new_batches_limit => non_neg_integer(), |
||||
:rollup_rpc => %{ |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:chunk_size => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}, |
||||
:data => %{:new_batches_start_block => non_neg_integer(), optional(any()) => any()}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, non_neg_integer()} |
||||
def discover_new_batches( |
||||
%{ |
||||
config: %{ |
||||
l1_rpc: l1_rpc_config, |
||||
rollup_rpc: rollup_rpc_config, |
||||
l1_sequencer_inbox_address: sequencer_inbox_address, |
||||
messages_to_blocks_shift: messages_to_blocks_shift, |
||||
new_batches_limit: new_batches_limit |
||||
}, |
||||
data: %{new_batches_start_block: start_block} |
||||
} = _state |
||||
) do |
||||
# Requesting the "latest" block instead of "safe" allows to catch new batches |
||||
# without latency. |
||||
{:ok, latest_block} = |
||||
IndexerHelper.get_block_number_by_tag( |
||||
"latest", |
||||
l1_rpc_config.json_rpc_named_arguments, |
||||
Rpc.get_resend_attempts() |
||||
) |
||||
|
||||
end_block = min(start_block + l1_rpc_config.logs_block_range - 1, latest_block) |
||||
|
||||
if start_block <= end_block do |
||||
log_info("Block range for new batches discovery: #{start_block}..#{end_block}") |
||||
|
||||
discover( |
||||
sequencer_inbox_address, |
||||
start_block, |
||||
end_block, |
||||
new_batches_limit, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) |
||||
|
||||
{:ok, end_block} |
||||
else |
||||
{:ok, start_block - 1} |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Discovers and imports historical batches of rollup transactions within a specified block range. |
||||
|
||||
This function determines the L1 block range for discovering historical batches |
||||
of rollup transactions. Within this range, it retrieves logs representing the |
||||
SequencerBatchDelivered events emitted by the SequencerInbox contract. These |
||||
logs are processed to identify the batches and their details. The function then |
||||
constructs comprehensive data structures for batches, lifecycle transactions, |
||||
rollup blocks, and rollup transactions. Additionally, it identifies L2-to-L1 |
||||
messages that have been committed within these batches and updates their status. |
||||
All discovered and processed data are then imported into the database, with the |
||||
process targeting only previously undiscovered batches to enhance efficiency. |
||||
|
||||
## Parameters |
||||
- A map containing: |
||||
- `config`: Configuration settings including the L1 rollup initialization block, |
||||
RPC configurations, SequencerInbox address, a shift for the message |
||||
to block number mapping, and a limit for new batches discovery. |
||||
- `data`: Contains the ending block number for the historical batch discovery. |
||||
|
||||
## Returns |
||||
- `{:ok, start_block}`: On successful discovery and processing, where `start_block` |
||||
is the calculated starting block for the discovery range, |
||||
indicating the need to consider another block range in the |
||||
next iteration of historical batch discovery. |
||||
- `{:ok, l1_rollup_init_block}`: If the discovery process has reached the rollup |
||||
initialization block, indicating that all batches |
||||
up to the rollup origins have been discovered and |
||||
no further action is needed. |
||||
""" |
||||
@spec discover_historical_batches(%{ |
||||
:config => %{ |
||||
:l1_rollup_init_block => non_neg_integer(), |
||||
:l1_rpc => %{ |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:logs_block_range => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
:l1_sequencer_inbox_address => binary(), |
||||
:messages_to_blocks_shift => non_neg_integer(), |
||||
:new_batches_limit => non_neg_integer(), |
||||
:rollup_rpc => %{ |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:chunk_size => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}, |
||||
:data => %{:historical_batches_end_block => any(), optional(any()) => any()}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, non_neg_integer()} |
||||
def discover_historical_batches( |
||||
%{ |
||||
config: %{ |
||||
l1_rpc: l1_rpc_config, |
||||
rollup_rpc: rollup_rpc_config, |
||||
l1_sequencer_inbox_address: sequencer_inbox_address, |
||||
messages_to_blocks_shift: messages_to_blocks_shift, |
||||
l1_rollup_init_block: l1_rollup_init_block, |
||||
new_batches_limit: new_batches_limit |
||||
}, |
||||
data: %{historical_batches_end_block: end_block} |
||||
} = _state |
||||
) do |
||||
if end_block >= l1_rollup_init_block do |
||||
start_block = max(l1_rollup_init_block, end_block - l1_rpc_config.logs_block_range + 1) |
||||
|
||||
log_info("Block range for historical batches discovery: #{start_block}..#{end_block}") |
||||
|
||||
discover_historical( |
||||
sequencer_inbox_address, |
||||
start_block, |
||||
end_block, |
||||
new_batches_limit, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) |
||||
|
||||
{:ok, start_block} |
||||
else |
||||
{:ok, l1_rollup_init_block} |
||||
end |
||||
end |
||||
|
||||
# Initiates the discovery process for batches within a specified block range. |
||||
# |
||||
# Invokes the actual discovery process for new batches by calling `do_discover` |
||||
# with the provided parameters. |
||||
# |
||||
# ## Parameters |
||||
# - `sequencer_inbox_address`: The SequencerInbox contract address. |
||||
# - `start_block`: The starting block number for discovery. |
||||
# - `end_block`: The ending block number for discovery. |
||||
# - `new_batches_limit`: Limit of new batches to process in one iteration. |
||||
# - `messages_to_blocks_shift`: Shift value for message to block number mapping. |
||||
# - `l1_rpc_config`: Configuration for L1 RPC calls. |
||||
# - `rollup_rpc_config`: Configuration for rollup RPC calls. |
||||
# |
||||
# ## Returns |
||||
# - N/A |
||||
defp discover( |
||||
sequencer_inbox_address, |
||||
start_block, |
||||
end_block, |
||||
new_batches_limit, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) do |
||||
do_discover( |
||||
sequencer_inbox_address, |
||||
start_block, |
||||
end_block, |
||||
new_batches_limit, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) |
||||
end |
||||
|
||||
# Initiates the historical discovery process for batches within a specified block range. |
||||
# |
||||
# Calls `do_discover` with parameters reversed for start and end blocks to |
||||
# process historical data. |
||||
# |
||||
# ## Parameters |
||||
# - `sequencer_inbox_address`: The SequencerInbox contract address. |
||||
# - `start_block`: The starting block number for discovery. |
||||
# - `end_block`: The ending block number for discovery. |
||||
# - `new_batches_limit`: Limit of new batches to process in one iteration. |
||||
# - `messages_to_blocks_shift`: Shift value for message to block number mapping. |
||||
# - `l1_rpc_config`: Configuration for L1 RPC calls. |
||||
# - `rollup_rpc_config`: Configuration for rollup RPC calls. |
||||
# |
||||
# ## Returns |
||||
# - N/A |
||||
defp discover_historical( |
||||
sequencer_inbox_address, |
||||
start_block, |
||||
end_block, |
||||
new_batches_limit, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) do |
||||
do_discover( |
||||
sequencer_inbox_address, |
||||
end_block, |
||||
start_block, |
||||
new_batches_limit, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) |
||||
end |
||||
|
||||
# Performs the discovery of new or historical batches within a specified block range, |
||||
# processing and importing the relevant data into the database. |
||||
# |
||||
# This function retrieves SequencerBatchDelivered event logs from the specified block range |
||||
# and processes these logs to identify new batches and their corresponding details. It then |
||||
# constructs comprehensive data structures for batches, lifecycle transactions, rollup |
||||
# blocks, and rollup transactions. Additionally, it identifies any L2-to-L1 messages that |
||||
# have been committed within these batches and updates their status. All discovered and |
||||
# processed data are then imported into the database. |
||||
# |
||||
# ## Parameters |
||||
# - `sequencer_inbox_address`: The SequencerInbox contract address used to filter logs. |
||||
# - `start_block`: The starting block number for the discovery range. |
||||
# - `end_block`: The ending block number for the discovery range. |
||||
# - `new_batches_limit`: The maximum number of new batches to process in one iteration. |
||||
# - `messages_to_blocks_shift`: The value used to align message counts with rollup block numbers. |
||||
# - `l1_rpc_config`: RPC configuration parameters for L1. |
||||
# - `rollup_rpc_config`: RPC configuration parameters for rollup data. |
||||
# |
||||
# ## Returns |
||||
# - N/A |
||||
defp do_discover( |
||||
sequencer_inbox_address, |
||||
start_block, |
||||
end_block, |
||||
new_batches_limit, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) do |
||||
raw_logs = |
||||
get_logs_new_batches( |
||||
min(start_block, end_block), |
||||
max(start_block, end_block), |
||||
sequencer_inbox_address, |
||||
l1_rpc_config.json_rpc_named_arguments |
||||
) |
||||
|
||||
logs = |
||||
if end_block >= start_block do |
||||
raw_logs |
||||
else |
||||
Enum.reverse(raw_logs) |
||||
end |
||||
|
||||
# Discovered logs are divided into chunks to ensure progress |
||||
# in batch discovery, even if an error interrupts the fetching process. |
||||
logs |
||||
|> Enum.chunk_every(new_batches_limit) |
||||
|> Enum.each(fn chunked_logs -> |
||||
{batches, lifecycle_txs, rollup_blocks, rollup_txs, committed_txs} = |
||||
handle_batches_from_logs( |
||||
chunked_logs, |
||||
messages_to_blocks_shift, |
||||
l1_rpc_config, |
||||
rollup_rpc_config |
||||
) |
||||
|
||||
{:ok, _} = |
||||
Chain.import(%{ |
||||
arbitrum_lifecycle_transactions: %{params: lifecycle_txs}, |
||||
arbitrum_l1_batches: %{params: batches}, |
||||
arbitrum_batch_blocks: %{params: rollup_blocks}, |
||||
arbitrum_batch_transactions: %{params: rollup_txs}, |
||||
arbitrum_messages: %{params: committed_txs}, |
||||
timeout: :infinity |
||||
}) |
||||
end) |
||||
end |
||||
|
||||
# Fetches logs for SequencerBatchDelivered events from the SequencerInbox contract within a block range. |
||||
# |
||||
# Retrieves logs that correspond to SequencerBatchDelivered events, specifically |
||||
# from the SequencerInbox contract, between the specified block numbers. |
||||
# |
||||
# ## Parameters |
||||
# - `start_block`: The starting block number for log retrieval. |
||||
# - `end_block`: The ending block number for log retrieval. |
||||
# - `sequencer_inbox_address`: The address of the SequencerInbox contract. |
||||
# - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
# |
||||
# ## Returns |
||||
# - A list of logs for SequencerBatchDelivered events within the specified block range. |
||||
defp get_logs_new_batches(start_block, end_block, sequencer_inbox_address, json_rpc_named_arguments) |
||||
when start_block <= end_block do |
||||
{:ok, logs} = |
||||
IndexerHelper.get_logs( |
||||
start_block, |
||||
end_block, |
||||
sequencer_inbox_address, |
||||
[@message_sequencer_batch_delivered], |
||||
json_rpc_named_arguments |
||||
) |
||||
|
||||
if length(logs) > 0 do |
||||
log_debug("Found #{length(logs)} SequencerBatchDelivered logs") |
||||
end |
||||
|
||||
logs |
||||
end |
||||
|
||||
# Processes logs to extract batch information and prepare it for database import. |
||||
# |
||||
# This function analyzes SequencerBatchDelivered event logs to identify new batches |
||||
# and retrieves their details, avoiding the reprocessing of batches already known |
||||
# in the database. It enriches the details of new batches with data from corresponding |
||||
# L1 transactions and blocks, including timestamps and block ranges. The function |
||||
# then prepares batches, associated rollup blocks and transactions, and lifecycle |
||||
# transactions for database import. Additionally, L2-to-L1 messages initiated in the |
||||
# rollup blocks associated with the discovered batches are retrieved from the database, |
||||
# marked as `:sent`, and prepared for database import. |
||||
# |
||||
# ## Parameters |
||||
# - `logs`: The list of SequencerBatchDelivered event logs. |
||||
# - `msg_to_block_shift`: The shift value for mapping batch messages to block numbers. |
||||
# - `l1_rpc_config`: The RPC configuration for L1 requests. |
||||
# - `rollup_rpc_config`: The RPC configuration for rollup data requests. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing lists of batches, lifecycle transactions, rollup blocks, |
||||
# rollup transactions, and committed messages (with the status `:sent`), all |
||||
# ready for database import. |
||||
defp handle_batches_from_logs( |
||||
logs, |
||||
msg_to_block_shift, |
||||
%{ |
||||
json_rpc_named_arguments: json_rpc_named_arguments, |
||||
chunk_size: chunk_size |
||||
} = l1_rpc_config, |
||||
rollup_rpc_config |
||||
) do |
||||
existing_batches = |
||||
logs |
||||
|> parse_logs_to_get_batch_numbers() |
||||
|> Db.batches_exist() |
||||
|
||||
{batches, txs_requests, blocks_requests} = parse_logs_for_new_batches(logs, existing_batches) |
||||
|
||||
blocks_to_ts = Rpc.execute_blocks_requests_and_get_ts(blocks_requests, json_rpc_named_arguments, chunk_size) |
||||
|
||||
{lifecycle_txs_wo_indices, batches_to_import} = |
||||
execute_tx_requests_parse_txs_calldata(txs_requests, msg_to_block_shift, blocks_to_ts, batches, l1_rpc_config) |
||||
|
||||
{blocks_to_import, rollup_txs_to_import} = get_rollup_blocks_and_transactions(batches_to_import, rollup_rpc_config) |
||||
|
||||
lifecycle_txs = |
||||
lifecycle_txs_wo_indices |
||||
|> Db.get_indices_for_l1_transactions() |
||||
|
||||
tx_counts_per_batch = batches_to_rollup_txs_amounts(rollup_txs_to_import) |
||||
|
||||
batches_list_to_import = |
||||
batches_to_import |
||||
|> Map.values() |
||||
|> Enum.reduce([], fn batch, updated_batches_list -> |
||||
[ |
||||
batch |
||||
|> Map.put(:commitment_id, get_l1_tx_id_by_hash(lifecycle_txs, batch.tx_hash)) |
||||
|> Map.put( |
||||
:transactions_count, |
||||
case tx_counts_per_batch[batch.number] do |
||||
nil -> 0 |
||||
value -> value |
||||
end |
||||
) |
||||
|> Map.drop([:tx_hash]) |
||||
| updated_batches_list |
||||
] |
||||
end) |
||||
|
||||
committed_txs = |
||||
blocks_to_import |
||||
|> Map.keys() |
||||
|> Enum.max() |
||||
|> get_committed_l2_to_l1_messages() |
||||
|
||||
{batches_list_to_import, Map.values(lifecycle_txs), Map.values(blocks_to_import), rollup_txs_to_import, |
||||
committed_txs} |
||||
end |
||||
|
||||
# Extracts batch numbers from logs of SequencerBatchDelivered events. |
||||
defp parse_logs_to_get_batch_numbers(logs) do |
||||
logs |
||||
|> Enum.map(fn event -> |
||||
{batch_num, _, _} = sequencer_batch_delivered_event_parse(event) |
||||
batch_num |
||||
end) |
||||
end |
||||
|
||||
# Parses logs representing SequencerBatchDelivered events to identify new batches. |
||||
# |
||||
# This function sifts through logs of SequencerBatchDelivered events, extracts the |
||||
# necessary data, and assembles a map of new batch descriptions. Additionally, it |
||||
# prepares RPC `eth_getTransactionByHash` and `eth_getBlockByNumber` requests to |
||||
# fetch details not present in the logs. To minimize subsequent RPC calls, only |
||||
# batches not previously known (i.e., absent in `existing_batches`) are processed. |
||||
# |
||||
# ## Parameters |
||||
# - `logs`: A list of event logs to be processed. |
||||
# - `existing_batches`: A list of batch numbers already processed. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - A map of new batch descriptions, which are not yet ready for database import. |
||||
# - A list of RPC `eth_getTransactionByHash` requests for fetching details of |
||||
# the L1 transactions associated with these batches. |
||||
# - A list of RPC requests to fetch details of the L1 blocks where these batches |
||||
# were included. |
||||
defp parse_logs_for_new_batches(logs, existing_batches) do |
||||
{batches, txs_requests, blocks_requests} = |
||||
logs |
||||
|> Enum.reduce({%{}, [], %{}}, fn event, {batches, txs_requests, blocks_requests} -> |
||||
{batch_num, before_acc, after_acc} = sequencer_batch_delivered_event_parse(event) |
||||
|
||||
tx_hash_raw = event["transactionHash"] |
||||
tx_hash = Rpc.string_hash_to_bytes_hash(tx_hash_raw) |
||||
blk_num = quantity_to_integer(event["blockNumber"]) |
||||
|
||||
if batch_num in existing_batches do |
||||
{batches, txs_requests, blocks_requests} |
||||
else |
||||
updated_batches = |
||||
Map.put( |
||||
batches, |
||||
batch_num, |
||||
%{ |
||||
number: batch_num, |
||||
before_acc: before_acc, |
||||
after_acc: after_acc, |
||||
tx_hash: tx_hash |
||||
} |
||||
) |
||||
|
||||
updated_txs_requests = [ |
||||
Rpc.transaction_by_hash_request(%{id: 0, hash: tx_hash_raw}) |
||||
| txs_requests |
||||
] |
||||
|
||||
updated_blocks_requests = |
||||
Map.put( |
||||
blocks_requests, |
||||
blk_num, |
||||
BlockByNumber.request(%{id: 0, number: blk_num}, false, true) |
||||
) |
||||
|
||||
log_info("New batch #{batch_num} found in #{tx_hash_raw}") |
||||
|
||||
{updated_batches, updated_txs_requests, updated_blocks_requests} |
||||
end |
||||
end) |
||||
|
||||
{batches, txs_requests, Map.values(blocks_requests)} |
||||
end |
||||
|
||||
# Parses SequencerBatchDelivered event to get batch sequence number and associated accumulators |
||||
defp sequencer_batch_delivered_event_parse(event) do |
||||
[_, batch_sequence_number, before_acc, after_acc] = event["topics"] |
||||
|
||||
{quantity_to_integer(batch_sequence_number), before_acc, after_acc} |
||||
end |
||||
|
||||
# Executes transaction requests and parses the calldata to extract batch data. |
||||
# |
||||
# This function processes a list of RPC `eth_getTransactionByHash` requests, extracts |
||||
# and decodes the calldata from the transactions to obtain batch details. It updates |
||||
# the provided batch map with block ranges for new batches and constructs a map of |
||||
# lifecycle transactions with their timestamps and finalization status. |
||||
# |
||||
# ## Parameters |
||||
# - `txs_requests`: The list of RPC requests to fetch transaction data. |
||||
# - `msg_to_block_shift`: The shift value to adjust the message count to the correct |
||||
# rollup block numbers. |
||||
# - `blocks_to_ts`: A map of block numbers to their timestamps, required to complete |
||||
# data for corresponding lifecycle transactions. |
||||
# - `batches`: The current batch data to be updated. |
||||
# - A configuration map containing JSON RPC arguments, a track finalization flag, |
||||
# and a chunk size for batch processing. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - A map of lifecycle (L1) transactions, which are not yet compatible with |
||||
# database import and require further processing. |
||||
# - An updated map of batch descriptions, also requiring further processing |
||||
# before database import. |
||||
defp execute_tx_requests_parse_txs_calldata(txs_requests, msg_to_block_shift, blocks_to_ts, batches, %{ |
||||
json_rpc_named_arguments: json_rpc_named_arguments, |
||||
track_finalization: track_finalization?, |
||||
chunk_size: chunk_size |
||||
}) do |
||||
txs_requests |
||||
|> Enum.chunk_every(chunk_size) |
||||
|> Enum.reduce({%{}, batches}, fn chunk, {l1_txs, updated_batches} -> |
||||
chunk |
||||
# each eth_getTransactionByHash will take time since it returns entire batch |
||||
# in `input` which is heavy because contains dozens of rollup blocks |
||||
|> Rpc.make_chunked_request(json_rpc_named_arguments, "eth_getTransactionByHash") |
||||
|> Enum.reduce({l1_txs, updated_batches}, fn resp, {txs_map, batches_map} -> |
||||
block_num = quantity_to_integer(resp["blockNumber"]) |
||||
tx_hash = Rpc.string_hash_to_bytes_hash(resp["hash"]) |
||||
|
||||
# Although they are called messages in the functions' ABI, in fact they are |
||||
# rollup blocks |
||||
{batch_num, prev_message_count, new_message_count} = |
||||
add_sequencer_l2_batch_from_origin_calldata_parse(resp["input"]) |
||||
|
||||
# In some cases extracted numbers for messages does not linked directly |
||||
# with rollup blocks, for this, the numbers are shifted by a value specific |
||||
# for particular rollup |
||||
updated_batches_map = |
||||
Map.put( |
||||
batches_map, |
||||
batch_num, |
||||
Map.merge(batches_map[batch_num], %{ |
||||
start_block: prev_message_count + msg_to_block_shift, |
||||
end_block: new_message_count + msg_to_block_shift - 1 |
||||
}) |
||||
) |
||||
|
||||
updated_txs_map = |
||||
Map.put(txs_map, tx_hash, %{ |
||||
hash: tx_hash, |
||||
block_number: block_num, |
||||
timestamp: blocks_to_ts[block_num], |
||||
status: |
||||
if track_finalization? do |
||||
:unfinalized |
||||
else |
||||
:finalized |
||||
end |
||||
}) |
||||
|
||||
{updated_txs_map, updated_batches_map} |
||||
end) |
||||
end) |
||||
end |
||||
|
||||
# Parses calldata of `addSequencerL2BatchFromOrigin` or `addSequencerL2BatchFromBlobs` |
||||
# functions to extract batch information. |
||||
defp add_sequencer_l2_batch_from_origin_calldata_parse(calldata) do |
||||
case calldata do |
||||
"0x8f111f3c" <> encoded_params -> |
||||
# addSequencerL2BatchFromOrigin(uint256 sequenceNumber, bytes calldata data, uint256 afterDelayedMessagesRead, address gasRefunder, uint256 prevMessageCount, uint256 newMessageCount) |
||||
[sequence_number, _data, _after_delayed_messages_read, _gas_refunder, prev_message_count, new_message_count] = |
||||
TypeDecoder.decode( |
||||
Base.decode16!(encoded_params, case: :lower), |
||||
%FunctionSelector{ |
||||
function: "addSequencerL2BatchFromOrigin", |
||||
types: [ |
||||
{:uint, 256}, |
||||
:bytes, |
||||
{:uint, 256}, |
||||
:address, |
||||
{:uint, 256}, |
||||
{:uint, 256} |
||||
] |
||||
} |
||||
) |
||||
|
||||
{sequence_number, prev_message_count, new_message_count} |
||||
|
||||
"0x3e5aa082" <> encoded_params -> |
||||
# addSequencerL2BatchFromBlobs(uint256 sequenceNumber, uint256 afterDelayedMessagesRead, address gasRefunder, uint256 prevMessageCount, uint256 newMessageCount) |
||||
[sequence_number, _after_delayed_messages_read, _gas_refunder, prev_message_count, new_message_count] = |
||||
TypeDecoder.decode( |
||||
Base.decode16!(encoded_params, case: :lower), |
||||
%FunctionSelector{ |
||||
function: "addSequencerL2BatchFromBlobs", |
||||
types: [ |
||||
{:uint, 256}, |
||||
{:uint, 256}, |
||||
:address, |
||||
{:uint, 256}, |
||||
{:uint, 256} |
||||
] |
||||
} |
||||
) |
||||
|
||||
{sequence_number, prev_message_count, new_message_count} |
||||
end |
||||
end |
||||
|
||||
# Retrieves rollup blocks and transactions for a list of batches. |
||||
# |
||||
# This function extracts rollup block ranges from each batch's data to determine |
||||
# the required blocks. It then fetches existing rollup blocks and transactions from |
||||
# the database and recovers any missing data through RPC if necessary. |
||||
# |
||||
# ## Parameters |
||||
# - `batches`: A list of batches, each containing rollup block ranges associated |
||||
# with the batch. |
||||
# - `rollup_rpc_config`: Configuration for RPC calls to fetch rollup data. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - A map of rollup blocks, ready for database import. |
||||
# - A list of rollup transactions, ready for database import. |
||||
defp get_rollup_blocks_and_transactions( |
||||
batches, |
||||
rollup_rpc_config |
||||
) do |
||||
blocks_to_batches = unwrap_rollup_block_ranges(batches) |
||||
|
||||
required_blocks_numbers = Map.keys(blocks_to_batches) |
||||
log_info("Identified #{length(required_blocks_numbers)} rollup blocks") |
||||
|
||||
{blocks_to_import_map, txs_to_import_list} = |
||||
get_rollup_blocks_and_txs_from_db(required_blocks_numbers, blocks_to_batches) |
||||
|
||||
# While it's not entirely aligned with data integrity principles to recover |
||||
# rollup blocks and transactions from RPC that are not yet indexed, it's |
||||
# a practical compromise to facilitate the progress of batch discovery. Given |
||||
# the potential high frequency of new batch appearances and the substantial |
||||
# volume of blocks and transactions, prioritizing discovery process advancement |
||||
# is deemed reasonable. |
||||
{blocks_to_import, txs_to_import} = |
||||
recover_data_if_necessary( |
||||
blocks_to_import_map, |
||||
txs_to_import_list, |
||||
required_blocks_numbers, |
||||
blocks_to_batches, |
||||
rollup_rpc_config |
||||
) |
||||
|
||||
log_info( |
||||
"Found #{length(Map.keys(blocks_to_import))} rollup blocks and #{length(txs_to_import)} rollup transactions in DB" |
||||
) |
||||
|
||||
{blocks_to_import, txs_to_import} |
||||
end |
||||
|
||||
# Unwraps rollup block ranges from batch data to create a block-to-batch number map. |
||||
# |
||||
# ## Parameters |
||||
# - `batches`: A map where keys are batch identifiers and values are structs |
||||
# containing the start and end blocks of each batch. |
||||
# |
||||
# ## Returns |
||||
# - A map where each key is a rollup block number and its value is the |
||||
# corresponding batch number. |
||||
defp unwrap_rollup_block_ranges(batches) do |
||||
batches |
||||
|> Map.values() |
||||
|> Enum.reduce(%{}, fn batch, b_2_b -> |
||||
batch.start_block..batch.end_block |
||||
|> Enum.reduce(b_2_b, fn block_num, b_2_b_inner -> |
||||
Map.put(b_2_b_inner, block_num, batch.number) |
||||
end) |
||||
end) |
||||
end |
||||
|
||||
# Retrieves rollup blocks and transactions from the database based on given block numbers. |
||||
# |
||||
# This function fetches rollup blocks from the database using provided block numbers. |
||||
# For each block, it constructs a map of rollup block details and a list of |
||||
# transactions, including the batch number from `blocks_to_batches` mapping, block |
||||
# hash, and transaction hash. |
||||
# |
||||
# ## Parameters |
||||
# - `rollup_blocks_numbers`: A list of rollup block numbers to retrieve from the |
||||
# database. |
||||
# - `blocks_to_batches`: A mapping from block numbers to batch numbers. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - A map of rollup blocks associated with the batch numbers, ready for |
||||
# database import. |
||||
# - A list of transactions, each associated with its respective rollup block |
||||
# and batch number, ready for database import. |
||||
defp get_rollup_blocks_and_txs_from_db(rollup_blocks_numbers, blocks_to_batches) do |
||||
rollup_blocks_numbers |
||||
|> Db.rollup_blocks() |
||||
|> Enum.reduce({%{}, []}, fn block, {blocks_map, txs_list} -> |
||||
batch_num = blocks_to_batches[block.number] |
||||
|
||||
updated_txs_list = |
||||
block.transactions |
||||
|> Enum.reduce(txs_list, fn tx, acc -> |
||||
[%{tx_hash: tx.hash.bytes, batch_number: batch_num} | acc] |
||||
end) |
||||
|
||||
updated_blocks_map = |
||||
blocks_map |
||||
|> Map.put(block.number, %{ |
||||
block_number: block.number, |
||||
batch_number: batch_num, |
||||
confirmation_id: nil |
||||
}) |
||||
|
||||
{updated_blocks_map, updated_txs_list} |
||||
end) |
||||
end |
||||
|
||||
# Recovers missing rollup blocks and transactions from the RPC if not all required blocks are found in the current data. |
||||
# |
||||
# This function compares the required rollup block numbers with the ones already |
||||
# present in the current data. If some blocks are missing, it retrieves them from |
||||
# the RPC along with their transactions. The retrieved blocks and transactions |
||||
# are then merged with the current data to ensure a complete set for further |
||||
# processing. |
||||
# |
||||
# ## Parameters |
||||
# - `current_rollup_blocks`: The map of rollup blocks currently held. |
||||
# - `current_rollup_txs`: The list of transactions currently held. |
||||
# - `required_blocks_numbers`: A list of block numbers that are required for |
||||
# processing. |
||||
# - `blocks_to_batches`: A map associating rollup block numbers with batch numbers. |
||||
# - `rollup_rpc_config`: Configuration for the RPC calls. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing the updated map of rollup blocks and the updated list of |
||||
# transactions, both are ready for database import. |
||||
defp recover_data_if_necessary( |
||||
current_rollup_blocks, |
||||
current_rollup_txs, |
||||
required_blocks_numbers, |
||||
blocks_to_batches, |
||||
rollup_rpc_config |
||||
) do |
||||
required_blocks_amount = length(required_blocks_numbers) |
||||
|
||||
found_blocks_numbers = Map.keys(current_rollup_blocks) |
||||
found_blocks_numbers_length = length(found_blocks_numbers) |
||||
|
||||
if found_blocks_numbers_length != required_blocks_amount do |
||||
log_info("Only #{found_blocks_numbers_length} of #{required_blocks_amount} rollup blocks found in DB") |
||||
|
||||
{recovered_blocks_map, recovered_txs_list, _} = |
||||
recover_rollup_blocks_and_txs_from_rpc( |
||||
required_blocks_numbers, |
||||
found_blocks_numbers, |
||||
blocks_to_batches, |
||||
rollup_rpc_config |
||||
) |
||||
|
||||
{Map.merge(current_rollup_blocks, recovered_blocks_map), current_rollup_txs ++ recovered_txs_list} |
||||
else |
||||
{current_rollup_blocks, current_rollup_txs} |
||||
end |
||||
end |
||||
|
||||
# Recovers missing rollup blocks and their transactions from RPC based on required block numbers. |
||||
# |
||||
# This function identifies missing rollup blocks by comparing the required block |
||||
# numbers with those already found. It then fetches the missing blocks in chunks |
||||
# using JSON RPC calls, aggregating the results into a map of rollup blocks and |
||||
# a list of transactions. The data is processed to ensure each block and its |
||||
# transactions are correctly associated with their batch number. |
||||
# |
||||
# ## Parameters |
||||
# - `required_blocks_numbers`: A list of block numbers that are required to be |
||||
# fetched. |
||||
# - `found_blocks_numbers`: A list of block numbers that have already been |
||||
# fetched. |
||||
# - `blocks_to_batches`: A map linking block numbers to their respective batch |
||||
# numbers. |
||||
# - `rollup_rpc_config`: A map containing configuration parameters including |
||||
# JSON RPC arguments for rollup RPC and the chunk size |
||||
# for batch processing. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - A map of rollup blocks associated with the batch numbers, ready for |
||||
# database import. |
||||
# - A list of transactions, each associated with its respective rollup block |
||||
# and batch number, ready for database import. |
||||
# - The updated counter of processed chunks (usually ignored). |
||||
defp recover_rollup_blocks_and_txs_from_rpc( |
||||
required_blocks_numbers, |
||||
found_blocks_numbers, |
||||
blocks_to_batches, |
||||
%{ |
||||
json_rpc_named_arguments: rollup_json_rpc_named_arguments, |
||||
chunk_size: rollup_chunk_size |
||||
} = _rollup_rpc_config |
||||
) do |
||||
missed_blocks = required_blocks_numbers -- found_blocks_numbers |
||||
missed_blocks_length = length(missed_blocks) |
||||
|
||||
missed_blocks |
||||
|> Enum.sort() |
||||
|> Enum.chunk_every(rollup_chunk_size) |
||||
|> Enum.reduce({%{}, [], 0}, fn chunk, {blocks_map, txs_list, chunks_counter} -> |
||||
Logging.log_details_chunk_handling( |
||||
"Collecting rollup data", |
||||
{"block", "blocks"}, |
||||
chunk, |
||||
chunks_counter, |
||||
missed_blocks_length |
||||
) |
||||
|
||||
requests = |
||||
chunk |
||||
|> Enum.reduce([], fn block_num, requests_list -> |
||||
[ |
||||
BlockByNumber.request( |
||||
%{ |
||||
id: blocks_to_batches[block_num], |
||||
number: block_num |
||||
}, |
||||
false |
||||
) |
||||
| requests_list |
||||
] |
||||
end) |
||||
|
||||
{blocks_map_updated, txs_list_updated} = |
||||
requests |
||||
|> Rpc.make_chunked_request_keep_id(rollup_json_rpc_named_arguments, "eth_getBlockByNumber") |
||||
|> prepare_rollup_block_map_and_transactions_list(blocks_map, txs_list) |
||||
|
||||
{blocks_map_updated, txs_list_updated, chunks_counter + length(chunk)} |
||||
end) |
||||
end |
||||
|
||||
# Processes JSON responses to construct a mapping of rollup block information and a list of transactions. |
||||
# |
||||
# This function takes JSON RPC responses for rollup blocks and processes each |
||||
# response to create a mapping of rollup block details and a comprehensive list |
||||
# of transactions associated with these blocks. It ensures that each block and its |
||||
# corresponding transactions are correctly associated with their batch number. |
||||
# |
||||
# ## Parameters |
||||
# - `json_responses`: A list of JSON RPC responses containing rollup block data. |
||||
# - `rollup_blocks`: The initial map of rollup block information. |
||||
# - `rollup_txs`: The initial list of rollup transactions. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - An updated map of rollup blocks associated with their batch numbers, ready |
||||
# for database import. |
||||
# - An updated list of transactions, each associated with its respective rollup |
||||
# block and batch number, ready for database import. |
||||
defp prepare_rollup_block_map_and_transactions_list(json_responses, rollup_blocks, rollup_txs) do |
||||
json_responses |
||||
|> Enum.reduce({rollup_blocks, rollup_txs}, fn resp, {blocks_map, txs_list} -> |
||||
batch_num = resp.id |
||||
blk_num = quantity_to_integer(resp.result["number"]) |
||||
|
||||
updated_blocks_map = |
||||
Map.put( |
||||
blocks_map, |
||||
blk_num, |
||||
%{block_number: blk_num, batch_number: batch_num, confirmation_id: nil} |
||||
) |
||||
|
||||
updated_txs_list = |
||||
case resp.result["transactions"] do |
||||
nil -> |
||||
txs_list |
||||
|
||||
new_txs -> |
||||
Enum.reduce(new_txs, txs_list, fn l2_tx_hash, txs_list -> |
||||
[%{tx_hash: l2_tx_hash, batch_number: batch_num} | txs_list] |
||||
end) |
||||
end |
||||
|
||||
{updated_blocks_map, updated_txs_list} |
||||
end) |
||||
end |
||||
|
||||
# Retrieves the unique identifier of an L1 transaction by its hash from the given |
||||
# map. `nil` if there is no such transaction in the map. |
||||
defp get_l1_tx_id_by_hash(l1_txs, hash) do |
||||
l1_txs |
||||
|> Map.get(hash) |
||||
|> Kernel.||(%{id: nil}) |
||||
|> Map.get(:id) |
||||
end |
||||
|
||||
# Aggregates rollup transactions by batch number, counting the number of transactions in each batch. |
||||
defp batches_to_rollup_txs_amounts(rollup_txs) do |
||||
rollup_txs |
||||
|> Enum.reduce(%{}, fn tx, acc -> |
||||
Map.put(acc, tx.batch_number, Map.get(acc, tx.batch_number, 0) + 1) |
||||
end) |
||||
end |
||||
|
||||
# Retrieves initiated L2-to-L1 messages up to specified block number and marks them as 'sent'. |
||||
defp get_committed_l2_to_l1_messages(block_number) do |
||||
block_number |
||||
|> Db.initiated_l2_to_l1_messages() |
||||
|> Enum.map(fn tx -> |
||||
Map.put(tx, :status, :sent) |
||||
end) |
||||
end |
||||
end |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,413 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Workers.NewL1Executions do |
||||
@moduledoc """ |
||||
Coordinates the discovery and processing of new and historical L2-to-L1 message executions for an Arbitrum rollup. |
||||
|
||||
This module is responsible for identifying and importing executions of messages |
||||
that were initiated from Arbitrum's Layer 2 (L2) and are to be relayed to |
||||
Layer 1 (L1). It handles both new executions that are currently occurring on L1 |
||||
and historical executions that occurred in the past but have not yet been |
||||
processed. |
||||
|
||||
Discovery of these message executions involves parsing logs for |
||||
`OutBoxTransactionExecuted` events emitted by the Arbitrum outbox contract. As |
||||
the logs do not provide comprehensive data for constructing the related |
||||
lifecycle transactions, the module executes batched RPC calls to |
||||
`eth_getBlockByNumber`, using the responses to obtain transaction timestamps, |
||||
thereby enriching the lifecycle transaction data. |
||||
""" |
||||
|
||||
import EthereumJSONRPC, only: [quantity_to_integer: 1] |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1, log_debug: 1] |
||||
|
||||
alias EthereumJSONRPC.Block.ByNumber, as: BlockByNumber |
||||
|
||||
import Explorer.Helper, only: [decode_data: 2] |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Utils.Helper, as: ArbitrumHelper |
||||
alias Indexer.Fetcher.Arbitrum.Utils.{Db, Rpc} |
||||
alias Indexer.Helper, as: IndexerHelper |
||||
|
||||
alias Explorer.Chain |
||||
|
||||
require Logger |
||||
|
||||
# keccak256("OutBoxTransactionExecuted(address,address,uint256,uint256)") |
||||
@outbox_transaction_executed_event "0x20af7f3bbfe38132b8900ae295cd9c8d1914be7052d061a511f3f728dab18964" |
||||
@outbox_transaction_executed_unindexed_params [{:uint, 256}] |
||||
|
||||
@doc """ |
||||
Discovers and processes new executions of L2-to-L1 messages within the current L1 block range. |
||||
|
||||
This function fetches logs for `OutBoxTransactionExecuted` events within the |
||||
specified L1 block range to identify new execution transactions for L2-to-L1 |
||||
messages, updating their status and linking them with corresponding lifecycle |
||||
transactions in the database. Additionally, the function checks unexecuted |
||||
L2-to-L1 messages to match them with any newly recorded executions and updates |
||||
their status to `:relayed`. |
||||
|
||||
## Parameters |
||||
- A map containing: |
||||
- `config`: Configuration settings including the Arbitrum outbox contract |
||||
address, JSON RPC arguments, and the block range for fetching |
||||
logs. |
||||
- `data`: Contains the starting block number for new execution discovery. |
||||
|
||||
## Returns |
||||
- `{:ok, end_block}`: On successful discovery and processing, where `end_block` |
||||
indicates the necessity to consider next block range in the |
||||
following iteration of new executions discovery. |
||||
- `{:ok, start_block - 1}`: when no new blocks on L1 produced from the last |
||||
iteration of the new executions discovery. |
||||
""" |
||||
@spec discover_new_l1_messages_executions(%{ |
||||
:config => %{ |
||||
:l1_outbox_address => binary(), |
||||
:l1_rpc => %{ |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:logs_block_range => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}, |
||||
:data => %{:new_executions_start_block => non_neg_integer(), optional(any()) => any()}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, non_neg_integer()} |
||||
def discover_new_l1_messages_executions( |
||||
%{ |
||||
config: %{ |
||||
l1_rpc: l1_rpc_config, |
||||
l1_outbox_address: outbox_address |
||||
}, |
||||
data: %{new_executions_start_block: start_block} |
||||
} = _state |
||||
) do |
||||
# Requesting the "latest" block instead of "safe" allows to catch executions |
||||
# without latency. |
||||
{:ok, latest_block} = |
||||
IndexerHelper.get_block_number_by_tag( |
||||
"latest", |
||||
l1_rpc_config.json_rpc_named_arguments, |
||||
Rpc.get_resend_attempts() |
||||
) |
||||
|
||||
end_block = min(start_block + l1_rpc_config.logs_block_range - 1, latest_block) |
||||
|
||||
if start_block <= end_block do |
||||
log_info("Block range for new l2-to-l1 messages executions discovery: #{start_block}..#{end_block}") |
||||
|
||||
discover(outbox_address, start_block, end_block, l1_rpc_config) |
||||
|
||||
{:ok, end_block} |
||||
else |
||||
{:ok, start_block - 1} |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Discovers and processes historical executions of L2-to-L1 messages within a calculated L1 block range. |
||||
|
||||
This function fetches logs for `OutBoxTransactionExecuted` events within the |
||||
calculated L1 block range. It then processes these logs to identify execution |
||||
transactions for L2-to-L1 messages, updating their status and linking them with |
||||
corresponding lifecycle transactions in the database. Additionally, the |
||||
function goes through unexecuted L2-to-L1 messages, matches them with the |
||||
executions recorded in the database up to this moment, and updates the messages' |
||||
status to `:relayed`. |
||||
|
||||
## Parameters |
||||
- A map containing: |
||||
- `config`: Configuration settings including the Arbitrum outbox contract |
||||
address, the initialization block for the rollup, and JSON RPC |
||||
arguments. |
||||
- `data`: Contains the ending block number for the historical execution |
||||
discovery. |
||||
|
||||
## Returns |
||||
- `{:ok, start_block}`: On successful discovery and processing, where |
||||
`start_block` indicates the necessity to consider another block range in the |
||||
next iteration of historical executions discovery. |
||||
- `{:ok, l1_rollup_init_block}`: If the historical discovery process has reached |
||||
the rollup initialization block, indicating that no further action is needed. |
||||
""" |
||||
@spec discover_historical_l1_messages_executions(%{ |
||||
:config => %{ |
||||
:l1_outbox_address => binary(), |
||||
:l1_rollup_init_block => non_neg_integer(), |
||||
:l1_rpc => %{ |
||||
:json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:logs_block_range => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}, |
||||
:data => %{:historical_executions_end_block => non_neg_integer(), optional(any()) => any()}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, non_neg_integer()} |
||||
def discover_historical_l1_messages_executions( |
||||
%{ |
||||
config: %{ |
||||
l1_rpc: l1_rpc_config, |
||||
l1_outbox_address: outbox_address, |
||||
l1_rollup_init_block: l1_rollup_init_block |
||||
}, |
||||
data: %{historical_executions_end_block: end_block} |
||||
} = _state |
||||
) do |
||||
if end_block >= l1_rollup_init_block do |
||||
start_block = max(l1_rollup_init_block, end_block - l1_rpc_config.logs_block_range + 1) |
||||
|
||||
log_info("Block range for historical l2-to-l1 messages executions discovery: #{start_block}..#{end_block}") |
||||
|
||||
discover(outbox_address, start_block, end_block, l1_rpc_config) |
||||
|
||||
{:ok, start_block} |
||||
else |
||||
{:ok, l1_rollup_init_block} |
||||
end |
||||
end |
||||
|
||||
# Discovers and imports execution transactions for L2-to-L1 messages within a specified L1 block range. |
||||
# |
||||
# This function fetches logs for `OutBoxTransactionExecuted` events within the |
||||
# specified L1 block range to discover new execution transactions. It processes |
||||
# these logs to extract execution details and associated lifecycle transactions, |
||||
# which are then imported into the database. For lifecycle timestamps not |
||||
# available in the logs, RPC calls to `eth_getBlockByNumber` are made to fetch |
||||
# the necessary data. Furthermore, the function checks unexecuted L2-to-L1 |
||||
# messages to match them with any recorded executions, updating their status to |
||||
# `:relayed` and establishing links with the corresponding lifecycle |
||||
# transactions. These updated messages are also imported into the database. |
||||
# |
||||
# ## Parameters |
||||
# - `outbox_address`: The address of the Arbitrum outbox contract to filter the |
||||
# logs. |
||||
# - `start_block`: The starting block number for log retrieval. |
||||
# - `end_block`: The ending block number for log retrieval. |
||||
# - `l1_rpc_config`: Configuration parameters including JSON RPC arguments and |
||||
# settings for processing the logs. |
||||
# |
||||
# ## Returns |
||||
# - N/A |
||||
defp discover(outbox_address, start_block, end_block, l1_rpc_config) do |
||||
logs = |
||||
get_logs_for_new_executions( |
||||
start_block, |
||||
end_block, |
||||
outbox_address, |
||||
l1_rpc_config.json_rpc_named_arguments |
||||
) |
||||
|
||||
{lifecycle_txs, executions} = get_executions_from_logs(logs, l1_rpc_config) |
||||
|
||||
unless executions == [] do |
||||
log_info("Executions for #{length(executions)} L2 messages will be imported") |
||||
|
||||
{:ok, _} = |
||||
Chain.import(%{ |
||||
arbitrum_lifecycle_transactions: %{params: lifecycle_txs}, |
||||
arbitrum_l1_executions: %{params: executions}, |
||||
timeout: :infinity |
||||
}) |
||||
end |
||||
|
||||
# Inspects all unexecuted messages to potentially mark them as completed, |
||||
# addressing the scenario where found executions may correspond to messages |
||||
# that have not yet been indexed. This ensures that as soon as a new unexecuted |
||||
# message is added to the database, it can be marked as relayed, considering |
||||
# the execution transactions that have already been indexed. |
||||
messages = get_relayed_messages(end_block) |
||||
|
||||
unless messages == [] do |
||||
log_info("Marking #{length(messages)} l2-to-l1 messages as completed") |
||||
|
||||
{:ok, _} = |
||||
Chain.import(%{ |
||||
arbitrum_messages: %{params: messages}, |
||||
timeout: :infinity |
||||
}) |
||||
end |
||||
end |
||||
|
||||
# Retrieves logs representing `OutBoxTransactionExecuted` events between the specified blocks. |
||||
defp get_logs_for_new_executions(start_block, end_block, outbox_address, json_rpc_named_arguments) |
||||
when start_block <= end_block do |
||||
{:ok, logs} = |
||||
IndexerHelper.get_logs( |
||||
start_block, |
||||
end_block, |
||||
outbox_address, |
||||
[@outbox_transaction_executed_event], |
||||
json_rpc_named_arguments |
||||
) |
||||
|
||||
if length(logs) > 0 do |
||||
log_debug("Found #{length(logs)} OutBoxTransactionExecuted logs") |
||||
end |
||||
|
||||
logs |
||||
end |
||||
|
||||
# Extracts and processes execution details from logs for L2-to-L1 message transactions. |
||||
# |
||||
# This function parses logs representing `OutBoxTransactionExecuted` events to |
||||
# extract basic execution details. It then requests block timestamps and |
||||
# associates them with the extracted executions, forming lifecycle transactions |
||||
# enriched with timestamps and finalization statuses. Subsequently, unique |
||||
# identifiers for the lifecycle transactions are determined, and the connection |
||||
# between execution records and lifecycle transactions is established. |
||||
# |
||||
# ## Parameters |
||||
# - `logs`: A collection of log entries to be processed. |
||||
# - `l1_rpc_config`: Configuration parameters including JSON RPC arguments, |
||||
# chunk size for RPC calls, and a flag indicating whether to track the |
||||
# finalization of transactions. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - A list of lifecycle transactions with updated timestamps, finalization |
||||
# statuses, and unique identifiers. |
||||
# - A list of detailed execution information for L2-to-L1 messages. |
||||
# Both lists are prepared for database importation. |
||||
defp get_executions_from_logs( |
||||
logs, |
||||
%{ |
||||
json_rpc_named_arguments: json_rpc_named_arguments, |
||||
chunk_size: chunk_size, |
||||
track_finalization: track_finalization? |
||||
} = _l1_rpc_config |
||||
) do |
||||
{basics_executions, basic_lifecycle_txs, blocks_requests} = parse_logs_for_new_executions(logs) |
||||
|
||||
blocks_to_ts = Rpc.execute_blocks_requests_and_get_ts(blocks_requests, json_rpc_named_arguments, chunk_size) |
||||
|
||||
lifecycle_txs = |
||||
basic_lifecycle_txs |
||||
|> ArbitrumHelper.extend_lifecycle_txs_with_ts_and_status(blocks_to_ts, track_finalization?) |
||||
|> Db.get_indices_for_l1_transactions() |
||||
|
||||
executions = |
||||
basics_executions |
||||
|> Enum.reduce([], fn execution, updated_executions -> |
||||
updated = |
||||
execution |
||||
|> Map.put(:execution_id, lifecycle_txs[execution.execution_tx_hash].id) |
||||
|> Map.drop([:execution_tx_hash]) |
||||
|
||||
[updated | updated_executions] |
||||
end) |
||||
|
||||
{Map.values(lifecycle_txs), executions} |
||||
end |
||||
|
||||
# Parses logs to extract new execution transactions for L2-to-L1 messages. |
||||
# |
||||
# This function processes log entries to identify `OutBoxTransactionExecuted` |
||||
# events, extracting the message ID, transaction hash, and block number for |
||||
# each. It accumulates this data into execution details, lifecycle |
||||
# transaction descriptions, and RPC requests for block information. These |
||||
# are then used in subsequent steps to finalize the execution status of the |
||||
# messages. |
||||
# |
||||
# ## Parameters |
||||
# - `logs`: A collection of log entries to be processed. |
||||
# |
||||
# ## Returns |
||||
# - A tuple containing: |
||||
# - `executions`: A list of details for execution transactions related to |
||||
# L2-to-L1 messages. |
||||
# - `lifecycle_txs`: A map of lifecycle transaction details, keyed by L1 |
||||
# transaction hash. |
||||
# - `blocks_requests`: A list of RPC requests for fetching block data where |
||||
# the executions occurred. |
||||
defp parse_logs_for_new_executions(logs) do |
||||
{executions, lifecycle_txs, blocks_requests} = |
||||
logs |
||||
|> Enum.reduce({[], %{}, %{}}, fn event, {executions, lifecycle_txs, blocks_requests} -> |
||||
msg_id = outbox_transaction_executed_event_parse(event) |
||||
|
||||
l1_tx_hash_raw = event["transactionHash"] |
||||
l1_tx_hash = Rpc.string_hash_to_bytes_hash(l1_tx_hash_raw) |
||||
l1_blk_num = quantity_to_integer(event["blockNumber"]) |
||||
|
||||
updated_executions = [ |
||||
%{ |
||||
message_id: msg_id, |
||||
execution_tx_hash: l1_tx_hash |
||||
} |
||||
| executions |
||||
] |
||||
|
||||
updated_lifecycle_txs = |
||||
Map.put( |
||||
lifecycle_txs, |
||||
l1_tx_hash, |
||||
%{hash: l1_tx_hash, block_number: l1_blk_num} |
||||
) |
||||
|
||||
updated_blocks_requests = |
||||
Map.put( |
||||
blocks_requests, |
||||
l1_blk_num, |
||||
BlockByNumber.request(%{id: 0, number: l1_blk_num}, false, true) |
||||
) |
||||
|
||||
log_debug("Execution for L2 message ##{msg_id} found in #{l1_tx_hash_raw}") |
||||
|
||||
{updated_executions, updated_lifecycle_txs, updated_blocks_requests} |
||||
end) |
||||
|
||||
{executions, lifecycle_txs, Map.values(blocks_requests)} |
||||
end |
||||
|
||||
# Parses `OutBoxTransactionExecuted` event data to extract the transaction index parameter |
||||
defp outbox_transaction_executed_event_parse(event) do |
||||
[transaction_index] = decode_data(event["data"], @outbox_transaction_executed_unindexed_params) |
||||
|
||||
transaction_index |
||||
end |
||||
|
||||
# Retrieves unexecuted messages from L2 to L1, marking them as completed if their |
||||
# corresponding execution transactions are identified. |
||||
# |
||||
# This function fetches messages confirmed on L1 up to the specified rollup block |
||||
# number and matches these messages with their corresponding execution transactions. |
||||
# For matched pairs, it updates the message status to `:relayed` and links them with |
||||
# the execution transactions. |
||||
# |
||||
# ## Parameters |
||||
# - `block_number`: The block number up to which messages are considered for |
||||
# completion. |
||||
# |
||||
# ## Returns |
||||
# - A list of messages marked as completed, ready for database import. |
||||
defp get_relayed_messages(block_number) do |
||||
# Assuming that both catchup block fetcher and historical messages catchup fetcher |
||||
# will check all discovered historical messages to be marked as executed it is not |
||||
# needed to handle :initiated and :sent of historical messages here, only for |
||||
# new messages discovered and changed their status from `:sent` to `:confirmed` |
||||
confirmed_messages = Db.confirmed_l2_to_l1_messages(block_number) |
||||
|
||||
if Enum.empty?(confirmed_messages) do |
||||
[] |
||||
else |
||||
log_debug("Identified #{length(confirmed_messages)} l2-to-l1 messages already confirmed but not completed") |
||||
|
||||
messages_map = |
||||
confirmed_messages |
||||
|> Enum.reduce(%{}, fn msg, acc -> |
||||
Map.put(acc, msg.message_id, msg) |
||||
end) |
||||
|
||||
messages_map |
||||
|> Map.keys() |
||||
|> Db.l1_executions() |
||||
|> Enum.map(fn execution -> |
||||
messages_map |
||||
|> Map.get(execution.message_id) |
||||
|> Map.put(:completion_transaction_hash, execution.execution_transaction.hash.bytes) |
||||
|> Map.put(:status, :relayed) |
||||
end) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,346 @@ |
||||
defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do |
||||
@moduledoc """ |
||||
Manages the discovery and processing of new and historical L1-to-L2 messages initiated on L1 for an Arbitrum rollup. |
||||
|
||||
This module is responsible for identifying and importing messages that are initiated |
||||
from Layer 1 (L1) to Arbitrum's Layer 2 (L2). It handles both new messages that are |
||||
currently being sent to L2 and historical messages that were sent in the past but |
||||
have not yet been processed by the system. |
||||
|
||||
The initiated messages are identified by analyzing logs associated with |
||||
`MessageDelivered` events emitted by the Arbitrum bridge contract. These logs |
||||
contain almost all the information required to compose the messages, except for the |
||||
originator's address, which is obtained by making an RPC call to get the transaction |
||||
details. |
||||
""" |
||||
|
||||
import EthereumJSONRPC, only: [quantity_to_integer: 1] |
||||
|
||||
import Explorer.Helper, only: [decode_data: 2] |
||||
|
||||
import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1, log_debug: 1] |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Utils.Rpc |
||||
alias Indexer.Helper, as: IndexerHelper |
||||
|
||||
alias Explorer.Chain |
||||
|
||||
require Logger |
||||
|
||||
@types_of_l1_messages_forwarded_to_l2 [3, 7, 9, 12] |
||||
|
||||
# keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)") |
||||
@message_delivered_event "0x5e3c1311ea442664e8b1611bfabef659120ea7a0a2cfc0667700bebc69cbffe1" |
||||
@message_delivered_event_unindexed_params [ |
||||
:address, |
||||
{:uint, 8}, |
||||
:address, |
||||
{:bytes, 32}, |
||||
{:uint, 256}, |
||||
{:uint, 64} |
||||
] |
||||
|
||||
@doc """ |
||||
Discovers new L1-to-L2 messages initiated on L1 within a configured block range and processes them for database import. |
||||
|
||||
This function calculates the block range for discovering new messages from L1 to L2 |
||||
based on the latest block number available on the network. It then fetches logs |
||||
related to L1-to-L2 events within this range, extracts message details from both |
||||
the log and the corresponding L1 transaction, and imports them into the database. |
||||
|
||||
## Parameters |
||||
- A map containing: |
||||
- `config`: Configuration settings including JSON RPC arguments for L1, Arbitrum |
||||
bridge address, RPC block range, and chunk size for RPC calls. |
||||
- `data`: Contains the starting block number for new L1-to-L2 message discovery. |
||||
|
||||
## Returns |
||||
- `{:ok, end_block}`: On successful discovery and processing, where `end_block` |
||||
indicates the necessity to consider next block range in the |
||||
following iteration of new message discovery. |
||||
- `{:ok, start_block - 1}`: when no new blocks on L1 produced from the last |
||||
iteration of the new message discovery. |
||||
""" |
||||
@spec discover_new_messages_to_l2(%{ |
||||
:config => %{ |
||||
:json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:l1_bridge_address => binary(), |
||||
:l1_rpc_block_range => non_neg_integer(), |
||||
:l1_rpc_chunk_size => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
:data => %{ |
||||
:new_msg_to_l2_start_block => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, non_neg_integer()} |
||||
def discover_new_messages_to_l2( |
||||
%{ |
||||
config: %{ |
||||
json_l1_rpc_named_arguments: json_rpc_named_arguments, |
||||
l1_rpc_chunk_size: chunk_size, |
||||
l1_rpc_block_range: rpc_block_range, |
||||
l1_bridge_address: bridge_address |
||||
}, |
||||
data: %{new_msg_to_l2_start_block: start_block} |
||||
} = _state |
||||
) do |
||||
# Requesting the "latest" block instead of "safe" allows to get messages originated to L2 |
||||
# much earlier than they will be seen by the Arbitrum Sequencer. |
||||
{:ok, latest_block} = |
||||
IndexerHelper.get_block_number_by_tag( |
||||
"latest", |
||||
json_rpc_named_arguments, |
||||
Rpc.get_resend_attempts() |
||||
) |
||||
|
||||
end_block = min(start_block + rpc_block_range - 1, latest_block) |
||||
|
||||
if start_block <= end_block do |
||||
log_info("Block range for discovery new messages from L1: #{start_block}..#{end_block}") |
||||
|
||||
discover( |
||||
bridge_address, |
||||
start_block, |
||||
end_block, |
||||
json_rpc_named_arguments, |
||||
chunk_size |
||||
) |
||||
|
||||
{:ok, end_block} |
||||
else |
||||
{:ok, start_block - 1} |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Discovers historical L1-to-L2 messages initiated on L1 within the configured block range and processes them for database import. |
||||
|
||||
This function calculates the block range for message discovery and targets historical |
||||
messages from L1 to L2 by querying the specified block range on L1. The discovery is |
||||
conducted by fetching logs related to L1-to-L2 events, extracting message details |
||||
from both the log and the corresponding L1 transaction, and importing them into |
||||
the database. |
||||
|
||||
## Parameters |
||||
- A map containing: |
||||
- `config`: Configuration settings including JSON RPC arguments for L1, Arbitrum |
||||
bridge address, rollup initialization block, block range, and chunk |
||||
size for RPC calls. |
||||
- `data`: Contains the end block for historical L1-to-L2 message discovery. |
||||
|
||||
## Returns |
||||
- `{:ok, start_block}`: On successful discovery and processing, where `start_block` |
||||
indicates the necessity to consider another block range in |
||||
the next iteration of message discovery. |
||||
- `{:ok, l1_rollup_init_block}`: If the discovery process already reached rollup |
||||
initialization block and no discovery action was |
||||
necessary. |
||||
""" |
||||
@spec discover_historical_messages_to_l2(%{ |
||||
:config => %{ |
||||
:json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), |
||||
:l1_bridge_address => binary(), |
||||
:l1_rollup_init_block => non_neg_integer(), |
||||
:l1_rpc_block_range => non_neg_integer(), |
||||
:l1_rpc_chunk_size => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
:data => %{ |
||||
:historical_msg_to_l2_end_block => non_neg_integer(), |
||||
optional(any()) => any() |
||||
}, |
||||
optional(any()) => any() |
||||
}) :: {:ok, non_neg_integer()} |
||||
def discover_historical_messages_to_l2( |
||||
%{ |
||||
config: %{ |
||||
json_l1_rpc_named_arguments: json_rpc_named_arguments, |
||||
l1_rpc_chunk_size: chunk_size, |
||||
l1_rpc_block_range: rpc_block_range, |
||||
l1_bridge_address: bridge_address, |
||||
l1_rollup_init_block: l1_rollup_init_block |
||||
}, |
||||
data: %{historical_msg_to_l2_end_block: end_block} |
||||
} = _state |
||||
) do |
||||
if end_block >= l1_rollup_init_block do |
||||
start_block = max(l1_rollup_init_block, end_block - rpc_block_range + 1) |
||||
|
||||
log_info("Block range for discovery historical messages from L1: #{start_block}..#{end_block}") |
||||
|
||||
discover( |
||||
bridge_address, |
||||
start_block, |
||||
end_block, |
||||
json_rpc_named_arguments, |
||||
chunk_size |
||||
) |
||||
|
||||
{:ok, start_block} |
||||
else |
||||
{:ok, l1_rollup_init_block} |
||||
end |
||||
end |
||||
|
||||
# Discovers and imports L1-to-L2 messages initiated on L1 within a specified block range. |
||||
# |
||||
# This function discovers messages initiated on L1 for transferring information from L1 to L2 |
||||
# by retrieving relevant logs within the specified block range on L1, focusing on |
||||
# `MessageDelivered` events. It processes these logs to extract and construct message |
||||
# details. For information not present in the events, RPC calls are made to fetch additional |
||||
# transaction details. The discovered messages are then imported into the database. |
||||
# |
||||
# ## Parameters |
||||
# - `bridge_address`: The address of the Arbitrum bridge contract used to filter the logs. |
||||
# - `start_block`: The starting block number for log retrieval. |
||||
# - `end_block`: The ending block number for log retrieval. |
||||
# - `json_rpc_named_argument`: Configuration parameters for the JSON RPC connection. |
||||
# - `chunk_size`: The size of chunks for processing RPC calls in batches. |
||||
# |
||||
# ## Returns |
||||
# - N/A |
||||
defp discover(bridge_address, start_block, end_block, json_rpc_named_argument, chunk_size) do |
||||
logs = |
||||
get_logs_for_l1_to_l2_messages( |
||||
start_block, |
||||
end_block, |
||||
bridge_address, |
||||
json_rpc_named_argument |
||||
) |
||||
|
||||
messages = get_messages_from_logs(logs, json_rpc_named_argument, chunk_size) |
||||
|
||||
unless messages == [] do |
||||
log_info("Origins of #{length(messages)} L1-to-L2 messages will be imported") |
||||
end |
||||
|
||||
{:ok, _} = |
||||
Chain.import(%{ |
||||
arbitrum_messages: %{params: messages}, |
||||
timeout: :infinity |
||||
}) |
||||
end |
||||
|
||||
# Retrieves logs representing the `MessageDelivered` events. |
||||
defp get_logs_for_l1_to_l2_messages(start_block, end_block, bridge_address, json_rpc_named_arguments) |
||||
when start_block <= end_block do |
||||
{:ok, logs} = |
||||
IndexerHelper.get_logs( |
||||
start_block, |
||||
end_block, |
||||
bridge_address, |
||||
[@message_delivered_event], |
||||
json_rpc_named_arguments |
||||
) |
||||
|
||||
if length(logs) > 0 do |
||||
log_debug("Found #{length(logs)} MessageDelivered logs") |
||||
end |
||||
|
||||
logs |
||||
end |
||||
|
||||
# Extracts complete message details from the provided logs and prepares them for |
||||
# database insertion. |
||||
# |
||||
# This function filters and parses the logs to identify L1-to-L2 messages, |
||||
# generating corresponding RPC requests to fetch additional transaction data. |
||||
# It executes these RPC requests to obtain the `from` address of each transaction. |
||||
# It then completes each message description by merging the fetched `from` |
||||
# address and setting the status to `:initiated`, making them ready for database |
||||
# import. |
||||
# |
||||
# ## Parameters |
||||
# - `logs`: A list of log entries to be processed. |
||||
# - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. |
||||
# - `chunk_size`: The size of chunks for batch processing transactions. |
||||
# |
||||
# ## Returns |
||||
# - A list of maps describing discovered messages compatible with the database |
||||
# import operation. |
||||
defp get_messages_from_logs(logs, json_rpc_named_arguments, chunk_size) do |
||||
{messages, txs_requests} = parse_logs_for_l1_to_l2_messages(logs) |
||||
|
||||
txs_to_from = Rpc.execute_transactions_requests_and_get_from(txs_requests, json_rpc_named_arguments, chunk_size) |
||||
|
||||
Enum.map(messages, fn msg -> |
||||
Map.merge(msg, %{ |
||||
originator_address: txs_to_from[msg.originating_transaction_hash], |
||||
status: :initiated |
||||
}) |
||||
end) |
||||
end |
||||
|
||||
# Parses logs to extract L1-to-L2 message details and prepares RPC requests for transaction data. |
||||
# |
||||
# This function processes log entries corresponding to `MessageDelivered` events, filtering out |
||||
# L1-to-L2 messages identified by one of the following message types: `3`, `17`, `9`, `12`. |
||||
# Utilizing information from both the transaction and the log, the function constructs maps |
||||
# that partially describe each message and prepares RPC `eth_getTransactionByHash` requests to fetch |
||||
# the remaining data needed to complete these message descriptions. |
||||
# |
||||
# ## Parameters |
||||
# - `logs`: A collection of log entries to be processed. |
||||
# |
||||
# ## Returns |
||||
# - A tuple comprising: |
||||
# - `messages`: A list of maps, each containing an incomplete representation of a message. |
||||
# - `txs_requests`: A list of RPC request `eth_getTransactionByHash` structured to fetch |
||||
# additional data needed to finalize the message descriptions. |
||||
defp parse_logs_for_l1_to_l2_messages(logs) do |
||||
{messages, txs_requests} = |
||||
logs |
||||
|> Enum.reduce({[], %{}}, fn event, {messages, txs_requests} -> |
||||
{msg_id, type, ts} = message_delivered_event_parse(event) |
||||
|
||||
if type in @types_of_l1_messages_forwarded_to_l2 do |
||||
tx_hash = event["transactionHash"] |
||||
blk_num = quantity_to_integer(event["blockNumber"]) |
||||
|
||||
updated_messages = [ |
||||
%{ |
||||
direction: :to_l2, |
||||
message_id: msg_id, |
||||
originating_transaction_hash: tx_hash, |
||||
origination_timestamp: ts, |
||||
originating_transaction_block_number: blk_num |
||||
} |
||||
| messages |
||||
] |
||||
|
||||
updated_txs_requests = |
||||
Map.put( |
||||
txs_requests, |
||||
tx_hash, |
||||
Rpc.transaction_by_hash_request(%{id: 0, hash: tx_hash}) |
||||
) |
||||
|
||||
log_debug("L1 to L2 message #{tx_hash} found with the type #{type}") |
||||
|
||||
{updated_messages, updated_txs_requests} |
||||
else |
||||
{messages, txs_requests} |
||||
end |
||||
end) |
||||
|
||||
{messages, Map.values(txs_requests)} |
||||
end |
||||
|
||||
# Parses the `MessageDelivered` event to extract relevant message details. |
||||
defp message_delivered_event_parse(event) do |
||||
[ |
||||
_inbox, |
||||
kind, |
||||
_sender, |
||||
_message_data_hash, |
||||
_base_fee_l1, |
||||
timestamp |
||||
] = decode_data(event["data"], @message_delivered_event_unindexed_params) |
||||
|
||||
message_index = quantity_to_integer(Enum.at(event["topics"], 1)) |
||||
|
||||
{message_index, kind, Timex.from_unix(timestamp)} |
||||
end |
||||
end |
@ -0,0 +1,44 @@ |
||||
defmodule Indexer.Transform.Arbitrum.Messaging do |
||||
@moduledoc """ |
||||
Helper functions for transforming data for Arbitrum cross-chain messages. |
||||
""" |
||||
|
||||
alias Indexer.Fetcher.Arbitrum.Messaging, as: ArbitrumMessages |
||||
|
||||
require Logger |
||||
|
||||
@doc """ |
||||
Parses and combines lists of rollup transactions and logs to identify and process both L1-to-L2 and L2-to-L1 messages. |
||||
|
||||
This function utilizes two filtering operations: one that identifies L1-to-L2 |
||||
message completions from a list of transactions and another that identifies |
||||
L2-to-L1 message initiations from a list of logs. Each filter constructs |
||||
a detailed message structure for the respective direction. The function then |
||||
combines these messages into a single list suitable for database import. |
||||
|
||||
## Parameters |
||||
- `transactions`: A list of rollup transaction entries to filter for L1-to-L2 messages. |
||||
- `logs`: A list of log entries to filter for L2-to-L1 messages. |
||||
|
||||
## Returns |
||||
- A combined list of detailed message maps from both L1-to-L2 completions and |
||||
L2-to-L1 initiations, ready for database import. |
||||
""" |
||||
@spec parse(list(), list()) :: list() |
||||
def parse(transactions, logs) do |
||||
prev_metadata = Logger.metadata() |
||||
Logger.metadata(fetcher: :arbitrum_bridge_l2) |
||||
|
||||
l1_to_l2_completion_ops = |
||||
transactions |
||||
|> ArbitrumMessages.filter_l1_to_l2_messages() |
||||
|
||||
l2_to_l1_initiating_ops = |
||||
logs |
||||
|> ArbitrumMessages.filter_l2_to_l1_messages() |
||||
|
||||
Logger.reset_metadata(prev_metadata) |
||||
|
||||
l1_to_l2_completion_ops ++ l2_to_l1_initiating_ops |
||||
end |
||||
end |
Loading…
Reference in new issue