From d86c2254625a7a8e1773e05ab7564d52894e4180 Mon Sep 17 00:00:00 2001 From: James Piechota Date: Mon, 13 Apr 2026 21:54:10 -0400 Subject: [PATCH 1/2] fix: bundler rejects messages if they aren't signed ans104 --- src/dev_bundler.erl | 173 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 139 insertions(+), 34 deletions(-) diff --git a/src/dev_bundler.erl b/src/dev_bundler.erl index dde4113d5..3330aac66 100644 --- a/src/dev_bundler.erl +++ b/src/dev_bundler.erl @@ -83,28 +83,54 @@ item(_Base, Req, Opts) -> verify_message(Req, Opts) -> case hb_message:with_only_committed(Req, Opts) of {ok, Item} -> - case hb_message:signers(Item, Opts) of - [] -> + case hb_message:commitment( + #{ <<"commitment-device">> => <<"ans104@1.0">> }, + Item, + Opts + ) of + not_found -> + ?event( + bundler_short, + {verify_failed, {reason, no_ans104_commitment}} + ), + {error, no_ans104_commitment}; + multiple_matches -> ?event( bundler_short, - {verify_failed, {reason, unsigned_item}} + {verify_failed, {reason, multiple_ans104_commitments}} ), - {error, unsigned_item}; - _ -> - case hb_message:verify(Item, all, Opts) of - true -> {ok, Item}; - false -> + {error, multiple_ans104_commitments}; + {ok, _, _} -> + case hb_message:signers(Item, Opts) of + [] -> ?event( bundler_short, - {verify_failed, - {id, - {string, hb_message:id(Item, signed, Opts)} - }, - {reason, signature_verification_failed} - }, - Opts + {verify_failed, {reason, unsigned_item}} ), - {error, signature_verification_failed} + {error, unsigned_item}; + _ -> + case hb_message:verify(Item, all, Opts) of + true -> {ok, Item}; + false -> + ?event( + bundler_short, + {verify_failed, + {id, + {string, + hb_message:id( + Item, + signed, + Opts + ) + } + }, + {reason, + signature_verification_failed} + }, + Opts + ), + {error, signature_verification_failed} + end end end; {error, Reason} -> @@ -624,23 +650,91 @@ unsigned_dataitem_test() -> store => hb_test_utils:test_store(), debug_print => false }), - Item = #tx{ + TestData = [ + #tx{ data = <<"testdata">>, - tags = [{<<"tag1">>, <<"value1">>}] + tags = [{<<"Tag1">>, <<"Value1">>}] }, - Response = post_data_item(Node, Item, ClientOpts), - ?assertMatch( - {error, #{ - <<"status">> := 400, - <<"error">> := <<"invalid-item">>, - <<"details">> := <<"unsigned-item">> - }}, - Response) + #tx{ + data = <<"testdata">>, + tags = [{<<"tag1">>, <<"value1">>}] + } + ], + lists:foreach( + fun(Item) -> + Response = post_data_item(Node, Item, ClientOpts), + ?assertMatch( + {error, #{ + <<"status">> := 400, + <<"error">> := <<"invalid-item">>, + <<"details">> := <<"no-ans104-commitment">> + }}, + Response + ) + end, + TestData + ) after %% Always cleanup, even if test fails stop_test_servers(ServerHandle) end. +unsupported_payload_types_test() -> + Anchor = rand:bytes(32), + Price = 12345, + {ServerHandle, NodeOpts} = start_mock_gateway( + #{ + price => {200, integer_to_binary(Price)}, + tx_anchor => {200, hb_util:encode(Anchor)} + } + ), + try + TestOpts = NodeOpts#{ + priv_wallet => ar_wallet:new(), + store => hb_test_utils:test_store(), + debug_print => false, + bundler_max_items => 1 + }, + Node = hb_http_server:start_node(TestOpts), + UnsupportedItems = [ + hb_message:commit( + #{ + <<"body">> => <<"httpsig-body">>, + <<"test-tag">> => <<"httpsig-signed-unsupported">> + }, + TestOpts, + <<"httpsig@1.0">> + ), + hb_message:commit( + #{ + <<"body">> => <<"httpsig-body">>, + <<"test-tag">> => <<"httpsig-unsigned-unsupported">> + }, + TestOpts, + #{ <<"device">> => <<"httpsig@1.0">>, <<"type">> => <<"unsigned">> } + ), + hb_message:commit( + #{ + <<"body">> => <<"tx-body">>, + <<"test-tag">> => <<"tx-signed-unsupported">> + }, + TestOpts, + #{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => true } + ) + ], + lists:foreach( + fun(Item) -> + ?assertMatch( + {error, #{ <<"status">> := 400 }}, + post_bundler_msg(Node, Item, TestOpts) + ) + end, + UnsupportedItems + ) + after + stop_test_servers(ServerHandle) + end. + idle_test() -> Anchor = rand:bytes(32), Price = 12345, @@ -1427,22 +1521,23 @@ post_data_item(Node, Item, Opts) -> <<"ans104@1.0">>, Opts ), + post_bundler_msg(Node, StructuredItem, Opts). + +post_bundler_msg(Node, Msg, Opts) -> hb_http:post( Node, #{ <<"path">> => <<"/~bundler@1.0/tx">>, <<"bundler-subject">> => <<"body">>, - <<"body">> => StructuredItem + <<"body">> => Msg }, Opts ). -assert_bundle(Node, ExpectedItems, Anchor, Price, TXRequest, Proofs, ClientOpts) -> - %% Reconstitute the transaction with its data from the POSTed payloads. +reconstitute_bundle_tx(TXRequest, Proofs) -> TXBinary = maps:get(<<"body">>, TXRequest), TXJSON = hb_json:decode(TXBinary), TXHeader = ar_tx:json_struct_to_tx(TXJSON), - %% Decode all chunks with their offsets, sort by offset, then concatenate ChunksWithOffsets = lists:map( fun(ChunkRequest) -> ProofBinary = maps:get(<<"body">>, ChunkRequest), @@ -1461,10 +1556,14 @@ assert_bundle(Node, ExpectedItems, Anchor, Price, TXRequest, Proofs, ClientOpts) end, Proofs ), - SortedChunks = lists:sort(fun({O1, _}, {O2, _}) -> O1 =< O2 end, ChunksWithOffsets), + SortedChunks = + lists:sort(fun({O1, _}, {O2, _}) -> O1 =< O2 end, ChunksWithOffsets), Chunks = [Chunk || {_Offset, Chunk} <- SortedChunks], DataBinary = iolist_to_binary(Chunks), - TX = TXHeader#tx{ data = DataBinary }, + TXHeader#tx{ data = DataBinary }. + +assert_bundle(Node, ExpectedItems, Anchor, Price, TXRequest, Proofs, ClientOpts) -> + TX = reconstitute_bundle_tx(TXRequest, Proofs), ?event(debug_test, {tx, TX}), ?assert(ar_tx:verify(TX)), ?assertEqual(Anchor, TX#tx.anchor), @@ -1474,18 +1573,24 @@ assert_bundle(Node, ExpectedItems, Anchor, Price, TXRequest, Proofs, ClientOpts) ?event(debug_test, {tx_structured, TXStructured}), ?assert(hb_message:verify(TXStructured, all, ClientOpts)), %% Verify individual data items in the bundle + {_ItemsBin, BundleIndex} = ar_bundles:decode_bundle_header(TX#tx.data), BundleDeserialized = ar_bundles:deserialize(TX), ?event(debug_test, {bundle_deserialized, BundleDeserialized}), ?assertEqual(length(ExpectedItems), maps:size(BundleDeserialized#tx.data)), %% Verify each data item's signature and match with expected items lists:foreach( - fun({Index, ExpectedItem}) -> + fun({Index, {IndexID, _Size}, ExpectedItem}) -> Key = integer_to_binary(Index), BundledItem = maps:get(Key, BundleDeserialized#tx.data), ?assert(ar_bundles:verify_item(BundledItem)), + ?assertEqual(IndexID, ar_bundles:id(BundledItem, signed)), ?assertEqual(ExpectedItem, BundledItem) end, - lists:zip(lists:seq(1, length(ExpectedItems)), ExpectedItems) + lists:zip3( + lists:seq(1, length(ExpectedItems)), + BundleIndex, + ExpectedItems + ) ), ?assertEqual(undefined, TX#tx.manifest), ?assertEqual(undefined, BundleDeserialized#tx.manifest), From 6281303f8a30a5ef3916da1dc4992780b6a1092b Mon Sep 17 00:00:00 2001 From: James Piechota Date: Fri, 17 Apr 2026 11:10:47 -0400 Subject: [PATCH 2/2] impr: simplify dev_bundler:verify_message, ensure test coverage --- src/dev_bundler.erl | 154 +++++++++++++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 36 deletions(-) diff --git a/src/dev_bundler.erl b/src/dev_bundler.erl index 3330aac66..ea9b4b324 100644 --- a/src/dev_bundler.erl +++ b/src/dev_bundler.erl @@ -84,7 +84,10 @@ verify_message(Req, Opts) -> case hb_message:with_only_committed(Req, Opts) of {ok, Item} -> case hb_message:commitment( - #{ <<"commitment-device">> => <<"ans104@1.0">> }, + #{ + <<"commitment-device">> => <<"ans104@1.0">>, + <<"committer">> => '_' + }, Item, Opts ) of @@ -101,36 +104,27 @@ verify_message(Req, Opts) -> ), {error, multiple_ans104_commitments}; {ok, _, _} -> - case hb_message:signers(Item, Opts) of - [] -> + case hb_message:verify(Item, all, Opts) of + true -> {ok, Item}; + false -> ?event( bundler_short, - {verify_failed, {reason, unsigned_item}} + {verify_failed, + {id, + {string, + hb_message:id( + Item, + signed, + Opts + ) + } + }, + {reason, + signature_verification_failed} + }, + Opts ), - {error, unsigned_item}; - _ -> - case hb_message:verify(Item, all, Opts) of - true -> {ok, Item}; - false -> - ?event( - bundler_short, - {verify_failed, - {id, - {string, - hb_message:id( - Item, - signed, - Opts - ) - } - }, - {reason, - signature_verification_failed} - }, - Opts - ), - {error, signature_verification_failed} - end + {error, signature_verification_failed} end end; {error, Reason} -> @@ -651,23 +645,29 @@ unsigned_dataitem_test() -> debug_print => false }), TestData = [ - #tx{ - data = <<"testdata">>, - tags = [{<<"Tag1">>, <<"Value1">>}] + { + #tx{ + data = <<"testdata">>, + tags = [{<<"Tag1">>, <<"Value1">>}] + }, + <<"no-ans104-commitment">> }, - #tx{ - data = <<"testdata">>, - tags = [{<<"tag1">>, <<"value1">>}] + { + #tx{ + data = <<"testdata">>, + tags = [{<<"tag1">>, <<"value1">>}] + }, + <<"no-ans104-commitment">> } ], lists:foreach( - fun(Item) -> + fun({Item, ExpectedDetails}) -> Response = post_data_item(Node, Item, ClientOpts), ?assertMatch( {error, #{ <<"status">> := 400, <<"error">> := <<"invalid-item">>, - <<"details">> := <<"no-ans104-commitment">> + <<"details">> := ExpectedDetails }}, Response ) @@ -679,6 +679,88 @@ unsigned_dataitem_test() -> stop_test_servers(ServerHandle) end. +verify_message_test() -> + VerifyOpts = #{ priv_wallet => ar_wallet:new() }, + SignedItem = ar_bundles:sign_item( + #tx{ + data = <<"testdata">>, + tags = [{<<"tag1">>, <<"value1">>}] + }, + ar_wallet:new() + ), + SignedMsg = hb_message:convert( + SignedItem, + <<"structured@1.0">>, + <<"ans104@1.0">>, + VerifyOpts + ), + ?assertMatch({ok, _}, verify_message(SignedMsg, VerifyOpts)), + {ok, CommitmentID, Commitment} = hb_message:commitment( + #{ <<"commitment-device">> => <<"ans104@1.0">> }, + SignedMsg, + VerifyOpts + ), + MultipleAns104Msg = + SignedMsg#{ + <<"commitments">> => maps:put( + <>, + Commitment, + maps:get(<<"commitments">>, SignedMsg) + ) + }, + ?assertEqual( + {error, multiple_ans104_commitments}, + verify_message(MultipleAns104Msg, VerifyOpts) + ), + UnsignedItem = + dev_arweave_common:normalize(#tx{ + data = <<"testdata">>, + tags = [ + {<<"ao-data-key">>, <<"body">>}, + {<<"Tag1">>, <<"Value1">>} + ] + }), + {ok, UnsignedMsg} = dev_codec_ans104:from(UnsignedItem, #{}, #{}), + ?assertEqual([], hb_message:signers(UnsignedMsg, #{})), + ?assertEqual( + {error, no_ans104_commitment}, + verify_message(UnsignedMsg, VerifyOpts) + ), + TamperedMsg = hb_message:convert( + SignedItem#tx{data = <<"tampereddata">>}, + <<"structured@1.0">>, + <<"ans104@1.0">>, + VerifyOpts + ), + ?assertEqual( + {error, signature_verification_failed}, + verify_message(TamperedMsg, VerifyOpts) + ), + NoAns104Msg = hb_message:commit( + #{ + <<"body">> => <<"httpsig-body">>, + <<"test-tag">> => <<"verify-message-no-ans104">> + }, + VerifyOpts, + <<"httpsig@1.0">> + ), + ?assertEqual( + {error, no_ans104_commitment}, + verify_message(NoAns104Msg, VerifyOpts) + ), + MalformedCommitmentsMsg = #{ + <<"body">> => <<"malformed">>, + <<"commitments">> => #{ + <<"bad">> => #{ + <<"commitment-device">> => <<"ans104@1.0">> + } + } + }, + ?assertMatch( + {error, {could_not_normalize, _, _, _, _}}, + verify_message(MalformedCommitmentsMsg, VerifyOpts) + ). + unsupported_payload_types_test() -> Anchor = rand:bytes(32), Price = 12345,