From dec7d24afb89e964371253b92f8f5994bd2eaeff Mon Sep 17 00:00:00 2001 From: speeddragon Date: Thu, 9 Jul 2026 15:17:38 +0100 Subject: [PATCH 1/4] impr: Allow to receive multiple responses from nodes when requesting arweave nodes for /block/current --- src/core/resolver/hb_opts.erl | 9 +++++ src/preloaded/arweave/dev_arweave.erl | 44 ++++++++++++++++++++- src/preloaded/query/dev_copycat_arweave.erl | 42 ++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/core/resolver/hb_opts.erl b/src/core/resolver/hb_opts.erl index b1acd6be8..c2a6c8c8f 100644 --- a/src/core/resolver/hb_opts.erl +++ b/src/core/resolver/hb_opts.erl @@ -426,6 +426,15 @@ raw_default_message() -> <<"opts">> => ?DEFAULT_HTTP_OPTS } }, + % Wait for 2 node responses when fetching current block information + #{ + <<"template">> => <<"^/arweave/block/current">>, + <<"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..08d3382b5 100644 --- a/src/preloaded/arweave/dev_arweave.erl +++ b/src/preloaded/arweave/dev_arweave.erl @@ -885,10 +885,20 @@ 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(<<"/block/current">>, Responses) when is_list(Responses) -> + case current_height_responses(Responses) of + [] -> best_response(Responses); + Heights -> current_height_best_response(Heights) + end; +best_response(_Path, Response) -> + best_response(Response). + best_response({error, {no_viable_responses, Responses}}) -> best_response(Responses); best_response([]) -> @@ -911,6 +921,38 @@ response_status(Response) when is_map(Response) -> response_status(_Response) -> 999. +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}}; + _ -> + false + catch _:_ -> + false + end; + (_) -> + false + end, + 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), 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, From f7062ab794c8ecf0e5d75cc88f622279627b000d Mon Sep 17 00:00:00 2001 From: speeddragon Date: Thu, 9 Jul 2026 17:34:45 +0100 Subject: [PATCH 2/4] impr: Support /height, improve code structure --- src/preloaded/arweave/dev_arweave.erl | 63 +++++++++++++++++---------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/preloaded/arweave/dev_arweave.erl b/src/preloaded/arweave/dev_arweave.erl index 08d3382b5..2bd67d1d7 100644 --- a/src/preloaded/arweave/dev_arweave.erl +++ b/src/preloaded/arweave/dev_arweave.erl @@ -891,11 +891,10 @@ request(Method, Path, Extra, LogExtra, Opts) -> %% 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(Responses); best_response(<<"/block/current">>, Responses) when is_list(Responses) -> - case current_height_responses(Responses) of - [] -> best_response(Responses); - Heights -> current_height_best_response(Heights) - end; + current_height_best_response(Responses); best_response(_Path, Response) -> best_response(Response). @@ -921,37 +920,47 @@ response_status(Response) when is_map(Response) -> response_status(_Response) -> 999. -current_height_responses(Responses) -> - lists:filtermap( +%% @doc Parse responses that contains block height to return the one with the +%% highest value. This helps when one node lags behind. +current_height_best_response(Responses) -> + FilteredResponsesHeight = 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 _:_ -> - false + case hb_util:safe_int(Body) of + {ok, Height} -> {true, {Height, Response}}; + {error, invalid} -> false + end end; (_) -> false end, 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. + ), + case FilteredResponsesHeight of + [] -> + {error, no_viable_responses}; + [{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 + end. %% @doc Transform a response from the Arweave node into an AO-Core message. to_message(Path, Method, {error, #{ <<"status">> := 404 }}, LogExtra, _Opts) -> @@ -1251,6 +1260,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, From ca801777ec8b35dcd9c6e577947d9d6eb83fccf3 Mon Sep 17 00:00:00 2001 From: speeddragon Date: Fri, 10 Jul 2026 14:53:26 +0100 Subject: [PATCH 3/4] fix: Unify both loops into one. Add /height route to request 2 responses --- src/core/resolver/hb_opts.erl | 10 +++++- src/preloaded/arweave/dev_arweave.erl | 49 +++++++++++++-------------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/core/resolver/hb_opts.erl b/src/core/resolver/hb_opts.erl index c2a6c8c8f..306967d5e 100644 --- a/src/core/resolver/hb_opts.erl +++ b/src/core/resolver/hb_opts.erl @@ -426,7 +426,7 @@ raw_default_message() -> <<"opts">> => ?DEFAULT_HTTP_OPTS } }, - % Wait for 2 node responses when fetching current block information + % Wait for 2 node responses when fetching current block or height information #{ <<"template">> => <<"^/arweave/block/current">>, <<"nodes">> => add_opts(?ARWEAVE_BOOTSTRAP_CHAIN_NODES), @@ -435,6 +435,14 @@ raw_default_message() -> <<"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 2bd67d1d7..e4b4b4e1e 100644 --- a/src/preloaded/arweave/dev_arweave.erl +++ b/src/preloaded/arweave/dev_arweave.erl @@ -923,42 +923,39 @@ response_status(_Response) -> %% @doc Parse responses that contains block height to return the one with the %% highest value. This helps when one node lags behind. current_height_best_response(Responses) -> - FilteredResponsesHeight = lists:filtermap( + Best = lists:foldl( 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}}; + ({ok, #{ <<"body">> := Body }} = Response, CurrentBest) -> + Height = try hb_json:decode(Body) of + #{ <<"height">> := DecodedHeight } -> + hb_util:int(DecodedHeight); + DecodedHeight when is_integer(DecodedHeight) -> + DecodedHeight; _ -> - false + undefined catch _:_ -> case hb_util:safe_int(Body) of - {ok, Height} -> {true, {Height, Response}}; - {error, invalid} -> false + {ok, ParsedHeight} -> ParsedHeight; + {error, invalid} -> undefined end + end, + case CurrentBest of + undefined when is_integer(Height) -> {Height, Response}; + {BestHeight, _} + when is_integer(Height), Height > BestHeight -> + {Height, Response}; + _ -> CurrentBest end; - (_) -> - false + (_, CurrentBest) -> + CurrentBest end, + undefined, Responses ), - case FilteredResponsesHeight of - [] -> + case Best of + undefined -> {error, no_viable_responses}; - [{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 - ), + {_Height, BestResponse} -> BestResponse end. From 6381030801d64b63dd7fb76e5c117090a9405b52 Mon Sep 17 00:00:00 2001 From: speeddragon Date: Fri, 10 Jul 2026 18:21:00 +0100 Subject: [PATCH 4/4] revert: Use previous code structure for checking the responses --- src/preloaded/arweave/dev_arweave.erl | 62 ++++++++++++++------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/preloaded/arweave/dev_arweave.erl b/src/preloaded/arweave/dev_arweave.erl index e4b4b4e1e..5b7ce743c 100644 --- a/src/preloaded/arweave/dev_arweave.erl +++ b/src/preloaded/arweave/dev_arweave.erl @@ -892,9 +892,9 @@ request(Method, Path, Extra, LogExtra, Opts) -> best_response(Path, {error, {no_viable_responses, Responses}}) -> best_response(Path, Responses); best_response(<<"/height">>, Responses) when is_list(Responses) -> - current_height_best_response(Responses); + current_height_best_response(current_height_responses(Responses)); best_response(<<"/block/current">>, Responses) when is_list(Responses) -> - current_height_best_response(Responses); + current_height_best_response(current_height_responses(Responses)); best_response(_Path, Response) -> best_response(Response). @@ -922,42 +922,44 @@ response_status(_Response) -> %% @doc Parse responses that contains block height to return the one with the %% highest value. This helps when one node lags behind. -current_height_best_response(Responses) -> - Best = lists:foldl( +current_height_responses(Responses) -> + lists:filtermap( fun - ({ok, #{ <<"body">> := Body }} = Response, CurrentBest) -> - Height = try hb_json:decode(Body) of - #{ <<"height">> := DecodedHeight } -> - hb_util:int(DecodedHeight); - DecodedHeight when is_integer(DecodedHeight) -> - DecodedHeight; + ({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}}; _ -> - undefined + false catch _:_ -> case hb_util:safe_int(Body) of - {ok, ParsedHeight} -> ParsedHeight; - {error, invalid} -> undefined + {ok, Height} -> {true, {Height, Response}}; + {error, invalid} -> false end - end, - case CurrentBest of - undefined when is_integer(Height) -> {Height, Response}; - {BestHeight, _} - when is_integer(Height), Height > BestHeight -> - {Height, Response}; - _ -> CurrentBest end; - (_, CurrentBest) -> - CurrentBest + (_) -> + false end, - undefined, Responses - ), - case Best of - undefined -> - {error, no_viable_responses}; - {_Height, BestResponse} -> - BestResponse - end. + ). + +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) ->