fix: avoid infinite loop during batch block range binary search (#10436)

pull/10465/head
Alexander Kolotov 4 months ago committed by GitHub
parent f87017793a
commit b6634290b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 206
      apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex

@ -12,6 +12,8 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
@zero_hash "0000000000000000000000000000000000000000000000000000000000000000"
@rpc_resend_attempts 20
@default_binary_search_threshold 1000
# outbox()
@selector_outbox "ce11e6ab"
# sequencerInbox()
@ -173,27 +175,13 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
EthereumJSONRPC.json_rpc_named_arguments()
) :: non_neg_integer()
def get_block_number_for_keyset(sequencer_inbox_address, keyset_hash, json_rpc_named_arguments) do
[
%{
contract_address: sequencer_inbox_address,
method_id: @selector_get_keyset_creation_block,
args: [keyset_hash]
}
]
|> IndexerHelper.read_contracts_with_retries(
read_contract_and_handle_result_as_integer(
sequencer_inbox_address,
@selector_get_keyset_creation_block,
[keyset_hash],
@selector_sequencer_inbox_contract_abi,
json_rpc_named_arguments,
@rpc_resend_attempts
json_rpc_named_arguments
)
# Extracts the list of responses from the tuple returned by read_contracts_with_retries.
|> Kernel.elem(0)
# Retrieves the first response from the list of responses. The responses are in a list
# because read_contracts_with_retries accepts a list of method calls.
|> List.first()
# Extracts the result from the {status, result} tuple which is composed in EthereumJSONRPC.Encoder.decode_result.
|> Kernel.elem(1)
# Extracts the first decoded value from the result, which is a list, even if it contains only one value.
|> List.first()
end
# Calls getter functions on a rollup contract and collects their return values.
@ -220,6 +208,7 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
}
end)
|> IndexerHelper.read_contracts_with_retries(@rollup_contract_abi, json_rpc_named_arguments, @rpc_resend_attempts)
# Extracts the list of responses from the tuple returned by read_contracts_with_retries.
|> Kernel.elem(0)
|> Enum.zip(method_ids)
|> Enum.reduce(%{}, fn {{:ok, [response]}, method_id}, retval ->
@ -473,12 +462,13 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
) do
opposite_block =
do_binary_search_of_opposite_block(
initial_block - initial_step,
max(1, initial_block - initial_step),
initial_step,
required_batch_number,
rollup_config,
required_batch_number,
initial_block
initial_block,
%{}
)
# the default direction for the block range exploration is chosen to be from the highest to lowest
@ -495,7 +485,8 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
# `findBatchContainingBlock` of the Node Interface contract to determine the
# batch number of the inspected block and, based on the call result and the
# previously inspected block, decides whether the opposite block is found or
# another iteration is required.
# another iteration is required. In order to avoid redundant RPC calls, the
# function uses a cache to store the batch numbers.
#
# Assumptions:
# - The initial step is low enough to not jump more than one batch in a single
@ -515,9 +506,26 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
# - `prev_batch_number`: The number of the batch where the block was inspected
# on the previous iteration.
# - `prev_inspected_block`: The block number that was previously inspected.
# - `cache`: A map that stores the batch numbers for rollup blocks to avoid
# redundant RPC calls.
# - `iteration_threshold`: The maximum number of iterations allowed for the
# binary search to avoid infinite loops.
#
# Returns:
# - The block number of the opposite block in the rollup.
# - The block number of the opposite block in the rollup or raises an error if
# the iteration threshold is exceeded.
@spec do_binary_search_of_opposite_block(
non_neg_integer(),
integer(),
non_neg_integer(),
%{
node_interface_address: EthereumJSONRPC.address(),
json_rpc_named_arguments: EthereumJSONRPC.json_rpc_named_arguments()
},
non_neg_integer(),
non_neg_integer(),
%{non_neg_integer() => non_neg_integer()}
) :: non_neg_integer()
@spec do_binary_search_of_opposite_block(
non_neg_integer(),
integer(),
@ -527,6 +535,8 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
json_rpc_named_arguments: EthereumJSONRPC.json_rpc_named_arguments()
},
non_neg_integer(),
non_neg_integer(),
%{non_neg_integer() => non_neg_integer()},
non_neg_integer()
) :: non_neg_integer()
defp do_binary_search_of_opposite_block(
@ -535,78 +545,158 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do
required_batch_number,
%{node_interface_address: _, json_rpc_named_arguments: _} = rollup_config,
prev_batch_number,
prev_inspected_block
prev_inspected_block,
cache,
iteration_threshold \\ @default_binary_search_threshold
) do
new_batch_number =
if iteration_threshold == 0 do
raise "Binary search iteration threshold exceeded"
end
{new_batch_number, new_cache} =
get_batch_number_for_rollup_block(
rollup_config.node_interface_address,
inspected_block,
rollup_config.json_rpc_named_arguments
rollup_config.json_rpc_named_arguments,
cache
)
next_block_to_inspect = max(1, inspected_block - step)
is_batch_repeated? = new_batch_number == prev_batch_number
is_min_step_required_batch? =
abs(prev_inspected_block - inspected_block) == 1 and new_batch_number == required_batch_number
new_step =
cond do
# The batch number is the same as the previous one, so there is no need to reduce step and
# the next iteration should continue in the same direction.
is_batch_repeated? ->
step
# For the next two cases the batch number differs from one found in the previous iteration,
# so it is necessary to cut the step in half and change the direction of the search if the
# the next iteration assumed to move away from the required batch number.
step > 0 ->
adjust_step(step, new_batch_number <= required_batch_number)
step < 0 ->
adjust_step(step, new_batch_number >= required_batch_number)
end
if is_min_step_required_batch? and not is_batch_repeated? do
# The current step is the smallest possible, the inspected block in the required batch but
# the batch number is different from one found in the previous iteration. This means that
# the previous block was in the neighboring batch and the current block is in the boundary
# of the required batch.
inspected_block
else
# Whether the required batch number is not reached yet, or there is uncertainty if the
# inspected block is in the boundary of the required batch: the current batch is the same
# as one found in the previous iteration or the step is not the smallest possible.
next_block_to_inspect = max(1, inspected_block - new_step)
if new_batch_number == prev_batch_number do
do_binary_search_of_opposite_block(
next_block_to_inspect,
step,
new_step,
required_batch_number,
rollup_config,
new_batch_number,
inspected_block
inspected_block,
new_cache,
iteration_threshold - 1
)
else
if abs(prev_inspected_block - inspected_block) == 1 and new_batch_number == required_batch_number do
inspected_block
else
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
new_step = if(abs(step) == 1, do: -step, else: -div(step, 2))
do_binary_search_of_opposite_block(
next_block_to_inspect,
new_step,
required_batch_number,
rollup_config,
new_batch_number,
inspected_block
)
end
end
end
# Adjusts the step size for the binary search based on the current step size and
# the need to change the direction of the search.
@spec adjust_step(integer(), boolean()) :: integer()
defp adjust_step(step, change_direction?) do
case {abs(step), change_direction?} do
{1, true} -> -step
{1, false} -> step
{_, true} -> -div(step, 2)
{_, false} -> div(step, 2)
end
end
# Retrieves the batch number for a given rollup block by interacting with the
# node interface contract. It calls the `findBatchContainingBlock` method of
# the contract to find the batch containing the specified block number.
# Node Interface contract.
#
# This function calls the `findBatchContainingBlock` method of the Node Interface
# contract to find the batch containing the specified block number. In order to
# avoid redundant RPC calls, the function uses a cache to store the batch numbers.
#
# Parameters:
# - `node_interface_address`: The address of the node interface contract.
# - `block_number`: The rollup block number.
# - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC
# connection.
# - `cache`: A map that stores the batch numbers for rollup blocks to avoid
# redundant RPC calls.
#
# Returns:
# - The number of a batch containing the specified rollup block.
# `{batch_number, new_cache}`, where
# - `batch_number` - The number of a batch containing the specified rollup block.
# - `new_cache` - The updated cache with the new batch number.
@spec get_batch_number_for_rollup_block(
EthereumJSONRPC.address(),
EthereumJSONRPC.block_number(),
EthereumJSONRPC.json_rpc_named_arguments(),
%{non_neg_integer() => non_neg_integer()}
) :: {non_neg_integer(), %{non_neg_integer() => non_neg_integer()}}
defp get_batch_number_for_rollup_block(node_interface_address, block_number, json_rpc_named_arguments, cache)
defp get_batch_number_for_rollup_block(_, block_number, _, cache) when is_map_key(cache, block_number) do
{Map.get(cache, block_number), cache}
end
defp get_batch_number_for_rollup_block(node_interface_address, block_number, json_rpc_named_arguments, cache) do
batch_number =
read_contract_and_handle_result_as_integer(
node_interface_address,
@selector_find_batch_containing_block,
[block_number],
@node_interface_contract_abi,
json_rpc_named_arguments
)
{batch_number, Map.put(cache, block_number, batch_number)}
end
# Calls one contract method and processes the result as an integer.
@spec read_contract_and_handle_result_as_integer(
EthereumJSONRPC.address(),
binary(),
[term()],
[map()],
EthereumJSONRPC.json_rpc_named_arguments()
) :: non_neg_integer()
defp get_batch_number_for_rollup_block(node_interface_address, block_number, json_rpc_named_arguments) do
defp read_contract_and_handle_result_as_integer(
contract_address,
method_selector,
args,
abi,
json_rpc_named_arguments
) do
[
%{
contract_address: node_interface_address,
method_id: @selector_find_batch_containing_block,
args: [block_number]
contract_address: contract_address,
method_id: method_selector,
args: args
}
]
|> IndexerHelper.read_contracts_with_retries(
@node_interface_contract_abi,
json_rpc_named_arguments,
@rpc_resend_attempts
)
|> IndexerHelper.read_contracts_with_retries(abi, json_rpc_named_arguments, @rpc_resend_attempts)
# Extracts the list of responses from the tuple returned by read_contracts_with_retries.
|> Kernel.elem(0)
# Retrieves the first response from the list of responses. The responses are in a list
# because read_contracts_with_retries accepts a list of method calls.
|> List.first()
# Extracts the result from the {status, result} tuple which is composed in EthereumJSONRPC.Encoder.decode_result.
|> Kernel.elem(1)
# Extracts the first decoded value from the result, which is a list, even if it contains only one value.
|> List.first()
end

Loading…
Cancel
Save