diff --git a/src/core/resolver/hb_opts.erl b/src/core/resolver/hb_opts.erl index b1acd6be8..306967d5e 100644 --- a/src/core/resolver/hb_opts.erl +++ b/src/core/resolver/hb_opts.erl @@ -426,6 +426,23 @@ raw_default_message() -> <<"opts">> => ?DEFAULT_HTTP_OPTS } }, + % Wait for 2 node responses when fetching current block or height information + #{ + <<"template">> => <<"^/arweave/block/current">>, + <<"nodes">> => add_opts(?ARWEAVE_BOOTSTRAP_CHAIN_NODES), + <<"parallel">> => true, + <<"responses">> => 2, + <<"stop-after">> => true, + <<"admissible-status">> => 200 + }, + #{ + <<"template">> => <<"^/arweave/height">>, + <<"nodes">> => add_opts(?ARWEAVE_BOOTSTRAP_CHAIN_NODES), + <<"parallel">> => true, + <<"responses">> => 2, + <<"stop-after">> => true, + <<"admissible-status">> => 200 + }, % General Arweave requests: race all chain nodes, take % the first 200. #{ diff --git a/src/preloaded/arweave/dev_arweave.erl b/src/preloaded/arweave/dev_arweave.erl index 580b069cf..5b7ce743c 100644 --- a/src/preloaded/arweave/dev_arweave.erl +++ b/src/preloaded/arweave/dev_arweave.erl @@ -885,10 +885,19 @@ request(Method, Path, Extra, LogExtra, Opts) -> <<"cache-control">> => [<<"no-cache">>, <<"no-store">>] } ), - to_message(Path, Method, best_response(Res), LogExtra, Opts). + to_message(Path, Method, best_response(Path, Res), LogExtra, Opts). %% @doc Select the best response from a list of responses by sorting them %% ascending by HTTP status code. Returns the first (best) response tuple. +best_response(Path, {error, {no_viable_responses, Responses}}) -> + best_response(Path, Responses); +best_response(<<"/height">>, Responses) when is_list(Responses) -> + current_height_best_response(current_height_responses(Responses)); +best_response(<<"/block/current">>, Responses) when is_list(Responses) -> + current_height_best_response(current_height_responses(Responses)); +best_response(_Path, Response) -> + best_response(Response). + best_response({error, {no_viable_responses, Responses}}) -> best_response(Responses); best_response([]) -> @@ -911,6 +920,47 @@ response_status(Response) when is_map(Response) -> response_status(_Response) -> 999. +%% @doc Parse responses that contains block height to return the one with the +%% highest value. This helps when one node lags behind. +current_height_responses(Responses) -> + lists:filtermap( + fun + ({ok, #{ <<"body">> := Body }} = Response) -> + try hb_json:decode(Body) of + #{ <<"height">> := Height } -> + {true, {hb_util:int(Height), Response}}; + Height when is_integer(Height) -> + {true, {Height, Response}}; + _ -> + false + catch _:_ -> + case hb_util:safe_int(Body) of + {ok, Height} -> {true, {Height, Response}}; + {error, invalid} -> false + end + end; + (_) -> + false + end, + Responses + ). + +current_height_best_response([]) -> + {error, no_viable_responses}; +current_height_best_response([{Height, Response} | Rest]) -> + {_, BestResponse} = + lists:foldl( + fun({NextHeight, NextResponse}, {BestHeight, Best}) -> + case NextHeight > BestHeight of + true -> {NextHeight, NextResponse}; + false -> {BestHeight, Best} + end + end, + {Height, Response}, + Rest + ), + BestResponse. + %% @doc Transform a response from the Arweave node into an AO-Core message. to_message(Path, Method, {error, #{ <<"status">> := 404 }}, LogExtra, _Opts) -> event_request(Path, Method, 404, LogExtra), @@ -1209,6 +1259,16 @@ best_response_handles_failed_connect_entries_test_parallel() -> best_response(Responses) ). +best_response_height_integer_body_test_parallel() -> + Responses = [ + {ok, #{ <<"status">> => 200, <<"body">> => <<"5">> }}, + {ok, #{ <<"status">> => 200, <<"body">> => <<"8">> }} + ], + ?assertEqual( + {ok, #{ <<"status">> => 200, <<"body">> => <<"8">> }}, + best_response(<<"/height">>, Responses) + ). + best_response_non_map_error_round_trips_test_parallel() -> FailedConnect = {failed_connect, diff --git a/src/preloaded/query/dev_copycat_arweave.erl b/src/preloaded/query/dev_copycat_arweave.erl index e4ef5effd..e4b07a22f 100644 --- a/src/preloaded/query/dev_copycat_arweave.erl +++ b/src/preloaded/query/dev_copycat_arweave.erl @@ -1361,6 +1361,48 @@ latest_height_failure_test_parallel() -> hb_mock_server:stop(MockHandle) end. +latest_height_uses_highest_mockserver_height_test_parallel() -> + {ok, MockURL1, MockHandle1} = hb_mock_server:start([ + {"/block/current", block_current, {200, <<"{\"height\": 5}">>}} + ]), + {ok, MockURL2, MockHandle2} = hb_mock_server:start([ + {"/block/current", block_current, {200, <<"{\"height\": 8}">>}} + ]), + TestStore = hb_test_utils:test_store(), + Opts = #{ + <<"store">> => [TestStore], + <<"arweave-index-blocks">> => false, + <<"routes">> => [ + #{ + <<"template">> => <<"^/arweave">>, + <<"nodes">> => [ + #{ + <<"match">> => <<"^/arweave">>, + <<"with">> => MockURL1, + <<"opts">> => #{ <<"http-client">> => httpc } + }, + #{ + <<"match">> => <<"^/arweave">>, + <<"with">> => MockURL2, + <<"opts">> => #{ <<"http-client">> => httpc } + } + ], + <<"parallel">> => false, + <<"responses">> => 2, + <<"stop-after">> => false, + <<"admissible-status">> => 200 + } + ] + }, + try + ?assertEqual({ok, 8}, latest_height(Opts)), + [_] = hb_mock_server:get_requests(block_current, 1, MockHandle1), + [_] = hb_mock_server:get_requests(block_current, 1, MockHandle2) + after + hb_mock_server:stop(MockHandle1), + hb_mock_server:stop(MockHandle2) + end. + negative_resolved_height_test_parallel() -> {ok, MockURL, MockHandle} = hb_mock_server:start([ {"/block/current", block_current,