diff --git a/src/core/resolver/hb_message.erl b/src/core/resolver/hb_message.erl index 425eadbfd..eea24fad9 100644 --- a/src/core/resolver/hb_message.erl +++ b/src/core/resolver/hb_message.erl @@ -58,6 +58,7 @@ -module(hb_message). -export([id/1, id/2, id/3]). -export([convert/3, convert/4, uncommitted/1, uncommitted/2, committed/3]). +-export([add_bundle_hint/2, add_bundle_hint/3]). -export([with_only_committers/2, with_only_committers/3, commitment_devices/2]). -export([verify/1, verify/2, verify/3, paranoid_verify/2, paranoid_verify/3]). -export([commit/2, commit/3, signers/2, type/1, minimize/1]). @@ -102,6 +103,7 @@ convert(Msg, TargetFormat, SourceFormat, Opts) -> true -> hb_maps:without([<<"priv">>], Msg, Opts); false -> Msg end, + TargetFormat, SourceFormat, Opts ), @@ -110,8 +112,9 @@ convert(Msg, TargetFormat, SourceFormat, Opts) -> _ -> from_tabm(TABM, TargetFormat, OldPriv, Opts) end. -to_tabm(Msg, SourceFormat, Opts) -> - {SourceCodecMod, Params} = conversion_spec_to_req(SourceFormat, Opts), +to_tabm(Msg, TargetFormat, SourceFormat, Opts) -> + {SourceCodecMod, Params0} = conversion_spec_to_req(SourceFormat, Opts), + Params = add_bundle_hint(Params0, TargetFormat, Opts), % We use _from_ here because the codecs are labelled from the perspective % of their own format. `dev_codec_ans104:from/1' will convert _from_ % an ANS-104 message _into_ a TABM. @@ -121,6 +124,51 @@ to_tabm(Msg, SourceFormat, Opts) -> {ok, OtherTypeRes} -> OtherTypeRes end. +%% @doc Extract the device value from a conversion spec. +conversion_spec_device(Spec, _Default, _Opts) + when is_binary(Spec) orelse (Spec == tabm) -> + Spec; +conversion_spec_device(Spec, Default, Opts) when is_map(Spec) -> + hb_maps:get(<<"device">>, Spec, Default, Opts); +conversion_spec_device(_Spec, Default, _Opts) -> + Default. + +%% @doc Extend a structured->tabm source spec with the `bundle' flag and +%% `hint-device' implied by a hint spec, so the structured codec can decide +%% whether to load or offload children and can call the target codec's +%% `to_hint/3' callback at each node of the tree. +%% +%% `Spec' is the spec being extended (the source spec when converting). +%% `HintSpec' is the spec from which we should infer bundling +%% (target spec when converting). +add_bundle_hint(Spec, Opts) -> + add_bundle_hint(Spec, Spec, Opts). +add_bundle_hint(Spec, HintSpec, Opts) -> + WithBundle = + case maps:is_key(<<"bundle">>, Spec) of + true -> + Spec; + false -> + case + is_map(HintSpec) + andalso hb_maps:find(<<"bundle">>, HintSpec, Opts) + of + {ok, Bundle} -> Spec#{ <<"bundle">> => Bundle }; + _ -> Spec + end + end, + case maps:is_key(<<"hint-device">>, WithBundle) of + true -> + WithBundle; + false -> + case conversion_spec_device(HintSpec, undefined, Opts) of + HintDevice when is_binary(HintDevice) -> + WithBundle#{ <<"hint-device">> => HintDevice }; + _ -> + WithBundle + end + end. + from_tabm(Msg, TargetFormat, OldPriv, Opts) -> {TargetCodecMod, Params} = conversion_spec_to_req(TargetFormat, Opts), % We use the _to_ function here because each of the codecs we may call in @@ -147,17 +195,15 @@ restore_priv(Msg, OldPriv, Opts) -> %% Expects conversion spec to either be a binary codec name, or a map with a %% `device' key and other parameters. Additionally honors the `always_bundle' %% key in the node message if present. -conversion_spec_to_req(Spec, Opts) when is_binary(Spec) or (Spec == tabm) -> +conversion_spec_to_req(Spec, Opts) when is_binary(Spec) orelse (Spec == tabm) -> conversion_spec_to_req(#{ <<"device">> => Spec }, Opts); conversion_spec_to_req(Spec, Opts) -> try - Device = - hb_maps:get( - <<"device">>, - Spec, - no_codec_device_in_conversion_spec, - Opts - ), + Device = conversion_spec_device( + Spec, + no_codec_device_in_conversion_spec, + Opts + ), { case Device of tabm -> tabm; diff --git a/src/dev_message_bundle_test_vectors.erl b/src/dev_message_bundle_test_vectors.erl new file mode 100644 index 000000000..30f7710a9 --- /dev/null +++ b/src/dev_message_bundle_test_vectors.erl @@ -0,0 +1,179 @@ +%%% @doc A battery of test vectors exercising the `bundle' / `hint-device' +%%% machinery of the `message@1.0' device across a three-level message tree. +%%% +%%% The tree is built bottom-up; each level holds the level below it as a +%%% sub-message: +%%% +%%%
+%%% L1 (root) --> L2 (middle) --> L3 (leaf) --> #{}
+%%%
+%%%
+%%% Each level is built with one of four choices:
+%%%
+%%% - `bundle_true' -- committed (`ans104@1.0') with `bundle' => true
+%%% - `bundle_false' -- committed with `bundle' => false
+%%% - `no_bundle' -- committed with no `bundle' flag
+%%% - `uncommitted' -- not committed at all; a plain, unsigned map
+%%%
+%%% For every 4x4x4 permutation of build choices the suite checks:
+%%%
+%%% - verify/3: every level verifies in the state it was committed in.
+%%% - id/3: the root's id equals its sole commitment's key (or, for an
+%%% uncommitted root, the content-addressed unsigned id).
+%%% - convert/4 with target `bundle' none/true/false: the tree
+%%% round-trips through the `ans104@1.0' codec -- the standard
+%%% structured<->codec path -- and still verifies at every level. A
+%%% `bundle' on the conversion target applies only to the root; nested
+%%% subtrees follow their own commitments via `hint-device', so the
+%%% committed shape survives the round-trip.
+-module(dev_message_bundle_test_vectors).
+-include_lib("eunit/include/eunit.hrl").
+-include("include/hb.hrl").
+
+fresh_opts() ->
+ #{
+ <<"priv-wallet">> => hb:wallet(),
+ <<"store">> => hb_test_utils:test_store()
+ }.
+
+%% @doc Build one tree level from its build choice: commit the message with
+%% the `ans104@1.0' codec (with `bundle' => true, `bundle' => false, or no
+%% `bundle' flag), or -- for `uncommitted' -- leave it as a plain map.
+build_level(Msg, uncommitted, _Opts) ->
+ Msg;
+build_level(Msg, no_bundle, Opts) ->
+ hb_message:commit(Msg, Opts, #{ <<"device">> => <<"ans104@1.0">> });
+build_level(Msg, bundle_true, Opts) ->
+ hb_message:commit(
+ Msg,
+ Opts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => true }
+ );
+build_level(Msg, bundle_false, Opts) ->
+ hb_message:commit(
+ Msg,
+ Opts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => false }
+ ).
+
+%% @doc Build a three-level tree with the given per-level build choices.
+build_tree(B1, B2, B3, Opts) ->
+ L3 =
+ build_level(
+ #{
+ <<"l3-tag">> => <<"l3-value">>,
+ <<"inner">> => #{ <<"deep">> => <<"deep-value">> }
+ },
+ B3,
+ Opts
+ ),
+ L2 =
+ build_level(#{ <<"l2-tag">> => <<"l2-value">>, <<"l3">> => L3 }, B2, Opts),
+ build_level(#{ <<"l1-tag">> => <<"l1-value">>, <<"l2">> => L2 }, B1, Opts).
+
+%%% Test vector generator.
+
+%% @doc The {API, RequestBundle} operations run against every tree shape.
+operations() ->
+ [
+ {verify, none},
+ {id, none},
+ {convert, none},
+ {convert, true},
+ {convert, false}
+ ].
+
+%% @doc The per-level build choices a tree level can take.
+build_choices() ->
+ [bundle_true, bundle_false, no_bundle, uncommitted].
+
+%% @doc Generate the full grid: 4x4x4 tree shapes x the operation list.
+bundle_vectors_test_() ->
+ {timeout, 600,
+ [
+ {
+ test_label(B1, B2, B3, Api, ReqBundle),
+ fun() -> run(B1, B2, B3, Api, ReqBundle) end
+ }
+ ||
+ B1 <- build_choices(),
+ B2 <- build_choices(),
+ B3 <- build_choices(),
+ {Api, ReqBundle} <- operations()
+ ]
+ }.
+
+test_label(B1, B2, B3, Api, ReqBundle) ->
+ lists:flatten(
+ io_lib:format(
+ "L1=~p L2=~p L3=~p ~p req-bundle=~p",
+ [B1, B2, B3, Api, ReqBundle]
+ )
+ ).
+
+%% @doc Build the tree and run_test the chosen API.
+run(B1, B2, B3, Api, ReqBundle) ->
+ Opts = fresh_opts(),
+ Tree = build_tree(B1, B2, B3, Opts),
+ ?event(debug_test, {tree,
+ {label, test_label(B1, B2, B3, Api, ReqBundle)},
+ {built, Tree}}),
+ % Every freshly built tree must verify via the reliable per-node path,
+ % whatever per-level bundle permutation it was signed with.
+ ?assert(hb_message:verify(Tree, all, Opts)),
+ run_test(Api, ReqBundle, B1, B2, B3, Tree, Opts).
+
+%%% Per-API run_tests.
+
+%% `verify': every level of a validly-built tree verifies. The bundle
+%% state each subtree was committed in is reproduced per-node via
+%% `hint-device', so the verify request carries no `bundle'. (`run/3'
+%% already verifies the root, so this only adds the nested levels.)
+run_test(verify, _ReqBundle, _B1, _B2, _B3, Tree, Opts) ->
+ L2 = hb_maps:get(<<"l2">>, Tree, undefined, Opts),
+ ?assert(hb_message:verify(L2, all, Opts)),
+ L3 = hb_maps:get(<<"l3">>, L2, undefined, Opts),
+ ?assert(hb_message:verify(L3, all, Opts));
+
+%% `id':
+%% - committed root: `id/3' with `all' committers accumulates to the
+%% single commitment -- the id must equal the key under which it is
+%% stored in the root's commitments map.
+%% - uncommitted root: there are no commitments, so `id/3' falls back to
+%% the (content-addressed) unsigned id -- `all' committers must give
+%% the same result as the bare unsigned-id call.
+run_test(id, _ReqBundle, uncommitted, _B2, _B3, Tree, Opts) ->
+ ?assertEqual(
+ hb_message:id(Tree, none, Opts),
+ hb_message:id(Tree, all, Opts)
+ );
+run_test(id, _ReqBundle, _B1, _B2, _B3, Tree, Opts) ->
+ Id = hb_message:id(Tree, all, Opts),
+ Commitments = hb_maps:get(<<"commitments">>, Tree, #{}, Opts),
+ ?assertEqual([Id], maps:keys(Commitments));
+
+%% `convert': round-trip the tree through the `ans104@1.0' codec. Each subtree
+%% converts in the state its own commitment dictates (per-node) via
+%% `hint-device', so the `bundle' on the conversion target applies only to the
+%% root and the committed shape is preserved.
+run_test(convert, ReqBundle, _B1, _B2, _B3, Tree, Opts) ->
+ Encoded = hb_message:convert(Tree, convert_target(ReqBundle), Opts),
+ Restored =
+ hb_message:convert(
+ Encoded,
+ <<"structured@1.0">>,
+ <<"ans104@1.0">>,
+ Opts
+ ),
+ ?assert(hb_message:verify(Restored, all, Opts)),
+ L2 = hb_maps:get(<<"l2">>, Restored, undefined, Opts),
+ ?assert(hb_message:verify(L2, all, Opts)),
+ L3 = hb_maps:get(<<"l3">>, L2, undefined, Opts),
+ ?assert(hb_message:verify(L3, all, Opts)).
+
+%% @doc The convert target for a request-bundle value: the bare `ans104@1.0'
+%% codec, plus a forced `bundle' flag when one is given.
+convert_target(none) ->
+ <<"ans104@1.0">>;
+convert_target(ReqBundle) ->
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => ReqBundle }.
diff --git a/src/preloaded/arweave/dev_bundler.erl b/src/preloaded/arweave/dev_bundler.erl
index 98112fdd9..126226df0 100644
--- a/src/preloaded/arweave/dev_bundler.erl
+++ b/src/preloaded/arweave/dev_bundler.erl
@@ -659,6 +659,109 @@ nested_bundle_test_parallel() ->
stop_test_servers(ServerHandle, NodeOpts)
end.
+%% @doc End-to-end bundler test for a nested dataitem where the parent
+%% has bundle=false. The chile is posted on its own first.
+nested_unbundled_bundle_child_posted_test_parallel() ->
+ run_nested_unbundled_bundle_test(child_posted).
+
+%% @doc Like `nested_inlined_bundle_child_posted_test_parallel/0', but the
+%% child is never posted on its own.
+nested_unbundled_bundle_child_not_posted_test_parallel() ->
+ run_nested_unbundled_bundle_test(child_not_posted).
+
+run_nested_unbundled_bundle_test(Variant) ->
+ Anchor = rand:bytes(32),
+ Price = 12345,
+ % NodeOpts redirects arweave gateway requests to the mock server.
+ {ServerHandle, NodeOpts} = hb_mock_server:start_arweave_gateway(
+ #{
+ price => {200, integer_to_binary(Price)},
+ tx_anchor => {200, hb_util:encode(Anchor)}
+ }
+ ),
+ try
+ ClientOpts = #{ <<"priv-wallet">> => ar_wallet:new() },
+ NodeOpts2 = maps:merge(NodeOpts, #{ <<"bundler-max-items">> => 3 }),
+ Node = hb_http_server:start_node(NodeOpts2#{
+ <<"priv-wallet">> => ar_wallet:new(),
+ <<"store">> => hb_test_utils:test_store()
+ }),
+ %% Child: an `httpsig@1.0'-signed message
+ Child = hb_message:commit(
+ #{
+ <<"event">> => <<"is_admissible">>,
+ <<"reference">> => <<"ref-value">>,
+ <<"status-class">> => <<"success">>
+ },
+ ClientOpts,
+ #{ <<"device">> => <<"httpsig@1.0">> }
+ ),
+ ?assert(hb_message:verify(Child, all, ClientOpts)),
+ %% Parent: signed with `ans104@1.0' and `bundle' => false, so the
+ %% child is offloaded as a link in the parent's committed form.
+ Parent = hb_message:commit(
+ #{
+ <<"data-protocol">> => <<"ao">>,
+ <<"type">> => <<"Assignment">>,
+ <<"body">> => Child
+ },
+ ClientOpts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => false }
+ ),
+ ?assert(hb_message:verify(Parent, all, ClientOpts)),
+ %% Post the first bundle slot (per `Variant'), then the nested
+ %% parent, then a plain data item.
+ ?assertMatch({ok, _}, post_first_item(Node, Variant, Child, ClientOpts)),
+ ?assertMatch({ok, _}, post_structured_item(Node, Parent, ClientOpts)),
+ ?assertMatch({ok, _},
+ post_data_item(Node, new_data_item(2, 10), ClientOpts)),
+ %% The three items bundle into a single transaction.
+ TXs = hb_mock_server:get_requests(tx, 1, ServerHandle),
+ ?assertEqual(1, length(TXs)),
+ Proofs = hb_mock_server:get_requests(chunk, 1, ServerHandle),
+ ?assert(length(Proofs) >= 1),
+ %% Reconstitute the bundle TX and verify it carries three valid items.
+ TX = reconstitute_tx(hd(TXs), Proofs),
+ ?event(debug_test, {tx, TX}),
+ ?assert(ar_tx:verify(TX)),
+ ?assertEqual(Anchor, TX#tx.anchor),
+ ?assertEqual(Price, TX#tx.reward),
+ Bundle = ar_bundles:deserialize(TX),
+ ?assertEqual(3, maps:size(Bundle#tx.data)),
+ %% Each bundled item must still verify once decoded back to
+ %% `structured@1.0'.
+ maps:foreach(
+ fun(_Key, BundledItem) ->
+ Structured = hb_message:convert(
+ BundledItem,
+ <<"structured@1.0">>,
+ <<"ans104@1.0">>,
+ ClientOpts
+ ),
+ ?assert(hb_message:verify(Structured, all, ClientOpts))
+ end,
+ Bundle#tx.data
+ ),
+ %% The bundle TX must convert to `structured@1.0' and verify, then
+ %% round-trip back to `tx@1.0' without inflating its data.
+ TXStructured = hb_message:convert(
+ TX, <<"structured@1.0">>, <<"tx@1.0">>, ClientOpts),
+ ?assert(hb_message:verify(TXStructured, all, ClientOpts)),
+ TXRoundtrip = hb_message:convert(
+ TXStructured, <<"tx@1.0">>, <<"structured@1.0">>, ClientOpts),
+ ?assertEqual(byte_size(TX#tx.data), byte_size(TXRoundtrip#tx.data)),
+ ?assert(ar_tx:verify(TXRoundtrip)),
+ ok
+ after
+ %% Always cleanup, even if test fails
+ stop_test_servers(ServerHandle, NodeOpts)
+ end.
+
+post_first_item(Node, child_posted, Child, ClientOpts) ->
+ post_structured_item(Node, Child, ClientOpts);
+post_first_item(Node, child_not_posted, _Child, ClientOpts) ->
+ post_data_item(Node, new_data_item(1, 10), ClientOpts).
+
price_error_test_parallel() ->
test_api_error(#{
price => {500, <<"error">>},
@@ -1522,6 +1625,10 @@ post_data_item(Node, Item, Opts) ->
<<"ans104@1.0">>,
Opts
),
+ post_structured_item(Node, StructuredItem, Opts).
+
+%% @doc Post an already-`structured@1.0' message to the bundler endpoint.
+post_structured_item(Node, StructuredItem, Opts) ->
hb_http:post(
Node,
#{
@@ -1532,8 +1639,11 @@ post_data_item(Node, Item, Opts) ->
Opts
).
-assert_bundle(Node, ExpectedItems, Anchor, Price, TXRequest, Proofs, ClientOpts) ->
- %% Reconstitute the transaction with its data from the POSTed payloads.
+%% @doc Reconstitute a bundle transaction from a captured `tx' request and
+%% its `chunk' proof requests: decode the header, validate every chunk's
+%% merkle path, then concatenate the chunks in offset order to recover the
+%% transaction data.
+reconstitute_tx(TXRequest, Proofs) ->
TXBinary = maps:get(<<"body">>, TXRequest),
TXJSON = hb_json:decode(TXBinary),
TXHeader = ar_tx:json_struct_to_tx(TXJSON),
@@ -1558,8 +1668,11 @@ assert_bundle(Node, ExpectedItems, Anchor, Price, TXRequest, Proofs, ClientOpts)
),
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 = iolist_to_binary(Chunks) }.
+
+assert_bundle(Node, ExpectedItems, Anchor, Price, TXRequest, Proofs, ClientOpts) ->
+ %% Reconstitute the transaction with its data from the POSTed payloads.
+ TX = reconstitute_tx(TXRequest, Proofs),
?event(debug_test, {tx, TX}),
?assert(ar_tx:verify(TX)),
?assertEqual(Anchor, TX#tx.anchor),
diff --git a/src/preloaded/arweave/dev_bundler_task.erl b/src/preloaded/arweave/dev_bundler_task.erl
index cd00d989e..e7992ac9e 100644
--- a/src/preloaded/arweave/dev_bundler_task.erl
+++ b/src/preloaded/arweave/dev_bundler_task.erl
@@ -33,10 +33,7 @@ execute_task(#task{type = post_tx, data = Items, opts = Opts} = Task) ->
case build_signed_tx(Items, Opts) of
{ok, SignedTX} ->
Committed = hb_message:convert(
- SignedTX,
- #{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => true },
- #{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => true },
- Opts),
+ SignedTX, <<"structured@1.0">>, <<"tx@1.0">>, Opts),
?event(bundler_short, log_task(posting_tx,
Task,
[{tx, {explicit, hb_message:id(Committed, signed, Opts)}}]
@@ -185,7 +182,7 @@ build_signed_tx(Items, Opts) ->
data_items_to_tx(Items, Opts) ->
List = lists:map(
- fun(Item) ->
+ fun(Item) ->
hb_message:convert(
Item,
#{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => true },
@@ -251,71 +248,6 @@ format_timestamp() ->
Millisecs = (MegaSecs * 1000000 + Secs) * 1000 + (MicroSecs div 1000),
calendar:system_time_to_rfc3339(Millisecs, [{unit, millisecond}, {offset, "Z"}]).
-build_signed_tx_test() ->
- Anchor = rand:bytes(32),
- Price = 12345,
- {ServerHandle, NodeOpts} = hb_mock_server:start_arweave_gateway(#{
- price => {200, integer_to_binary(Price)},
- tx_anchor => {200, hb_util:encode(Anchor)}
- }),
- TestOpts = NodeOpts#{
- <<"priv-wallet">> => ar_wallet:new(),
- <<"store">> => hb_test_utils:test_store()
- },
- try
- Timestamp = 12344567,
- ListValue = [<<"a">>, <<"b">>, <<"c">>],
- StructuredItems = [
- #{
- <<"body">> => <<"body1">>,
- <<"tag1">> => <<"value1">>,
- <<"timestamp">> => Timestamp
- },
- #{
- <<"body">> => <<"body3">>,
- <<"tag3">> => <<"value3">>,
- <<"list">> => ListValue
- },
- #{
- <<"body">> => <<"body2">>,
- <<"tag2">> => <<"value2">>
- }
- ],
- Items = [
- hb_message:commit(
- Item,
- TestOpts,
- #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => true }
- )
- || Item <- StructuredItems],
- {ok, SignedTX} = build_signed_tx(Items, TestOpts),
- ?assert(ar_tx:verify(SignedTX)),
- ?assertEqual(Anchor, SignedTX#tx.anchor),
- ?assertEqual(Price, SignedTX#tx.reward),
- ?event(debug_test, {signed_tx, SignedTX}),
- BundledTX = ar_bundles:deserialize(SignedTX),
- ?event(debug_test, {bundled_tx, BundledTX}),
- BundledItems = hb_util:numbered_keys_to_list(BundledTX#tx.data, #{}),
- lists:foreach(
- fun(Item) ->
- ?assert(ar_bundles:verify_item(Item))
- end,
- BundledItems
- ),
- BundledStructuredItems = [
- hb_message:convert(
- Item,
- <<"structured@1.0">>,
- <<"ans104@1.0">>,
- TestOpts
- )
- || Item <- BundledItems],
- ?assertEqual(lists:reverse(Items), BundledStructuredItems),
- ok
- after
- hb_mock_server:stop(ServerHandle)
- end.
-
build_signed_tx_on_arbundles_js_test() ->
Anchor = rand:bytes(32),
Price = 12345,
@@ -351,14 +283,14 @@ build_signed_tx_on_arbundles_js_test() ->
?assert(ar_bundles:verify_item(BundledItem)),
% Convert both dataitems to structured messages
ItemStructured = hb_message:convert(Item,
- #{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => true },
- #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => true },
+ <<"structured@1.0">>,
+ <<"ans104@1.0">>,
TestOpts),
?event(debug_test, {item_structured, ItemStructured}),
?assert(hb_message:verify(ItemStructured, all, TestOpts)),
BundledItemStructured = hb_message:convert(BundledItem,
- #{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => true },
- #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => true },
+ <<"structured@1.0">>,
+ <<"ans104@1.0">>,
TestOpts),
?event(debug_test, {bundled_item_structured, BundledItemStructured}),
?assert(hb_message:verify(BundledItemStructured, all, TestOpts)),
@@ -371,15 +303,15 @@ build_signed_tx_on_arbundles_js_test() ->
?assert(ar_tx:verify(SignedTX)),
% Convert the signed TX to a structured message
StructuredTX = hb_message:convert(SignedTX,
- #{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => true },
- #{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => true },
+ <<"structured@1.0">>,
+ <<"tx@1.0">>,
TestOpts),
% ?event(debug_test, {structured_tx, StructuredTX}),
?assert(hb_message:verify(StructuredTX, all, TestOpts)),
% Convert back to an L1 TX
SignedTXRoundtrip = hb_message:convert(StructuredTX,
- #{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => true },
- #{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => true },
+ <<"tx@1.0">>,
+ <<"structured@1.0">>,
TestOpts),
?event(debug_test, {signed_tx_roundtrip, SignedTXRoundtrip}),
?assert(ar_tx:verify(SignedTXRoundtrip)),
@@ -388,3 +320,197 @@ build_signed_tx_on_arbundles_js_test() ->
after
hb_mock_server:stop(ServerHandle)
end.
+
+%% Test that a nested dataitem is handled correctly by the bundler flow.
+%% This test focuses in on the conversion that happens between building
+%% the signed bundle TX and building the bundle proofs.
+bundle_convert_real_data_test() ->
+ Item = inlined_broken_item(),
+ Anchor = rand:bytes(32),
+ Price = 12345,
+ {ServerHandle, NodeOpts} = hb_mock_server:start_arweave_gateway(#{
+ price => {200, integer_to_binary(Price)},
+ tx_anchor => {200, hb_util:encode(Anchor)}
+ }),
+ TestOpts = NodeOpts#{
+ <<"priv-wallet">> => ar_wallet:new(),
+ <<"store">> => hb_test_utils:test_store()
+ },
+ try
+ {ok, SignedTX} = build_signed_tx([Item], TestOpts),
+ ?assert(ar_tx:verify(SignedTX)),
+ Committed = hb_message:convert(
+ SignedTX, <<"structured@1.0">>, <<"tx@1.0">>, TestOpts),
+ %% This convert is exactly what build_proofs runs.
+ TX = hb_message:convert(
+ Committed, <<"tx@1.0">>, <<"structured@1.0">>, TestOpts),
+ ?assert(ar_tx:verify(TX))
+ after
+ hb_mock_server:stop(ServerHandle)
+ end.
+
+bundle_convert_minimal_test() ->
+ Anchor = rand:bytes(32),
+ Price = 12345,
+ {ServerHandle, NodeOpts} = hb_mock_server:start_arweave_gateway(#{
+ price => {200, integer_to_binary(Price)},
+ tx_anchor => {200, hb_util:encode(Anchor)}
+ }),
+ TestOpts = NodeOpts#{
+ <<"priv-wallet">> => ar_wallet:new(),
+ <<"store">> => hb_test_utils:test_store()
+ },
+ try
+ Item = hb_message:commit(
+ #{ <<"key">> => <<"value">>,
+ <<"body">> => #{ <<"a">> => <<"b">> } },
+ TestOpts, #{<<"device">> => <<"ans104@1.0">>}),
+ {ok, SignedTX} = build_signed_tx([Item], TestOpts),
+ ?assert(ar_tx:verify(SignedTX)),
+ Committed = hb_message:convert(
+ SignedTX, <<"structured@1.0">>, <<"tx@1.0">>, TestOpts),
+ TX = hb_message:convert(
+ Committed, <<"tx@1.0">>, <<"structured@1.0">>, TestOpts),
+ ?assert(ar_tx:verify(TX))
+ after
+ hb_mock_server:stop(ServerHandle)
+ end.
+
+%% @doc Drive a nested tree of items signed in mixed bundle states through
+%% the bundler flow: each child is signed with bundle=true OR bundle=false,
+%% then we build the bundle TX, sign it, convert through structured@1.0 and
+%% back to tx@1.0, and assert nothing was inflated and every commitment
+%% still verifies. This exercises the full `hint-device' plumbing across a
+%% mixed tree, mirroring the production scenario that motivated the fix.
+bundle_convert_mixed_tree_verify_test() ->
+ Anchor = rand:bytes(32),
+ Price = 12345,
+ {ServerHandle, NodeOpts} = hb_mock_server:start_arweave_gateway(#{
+ price => {200, integer_to_binary(Price)},
+ tx_anchor => {200, hb_util:encode(Anchor)}
+ }),
+ TestOpts = NodeOpts#{
+ <<"priv-wallet">> => ar_wallet:new(),
+ <<"store">> => hb_test_utils:test_store()
+ },
+ try
+ %% Build three items. The first carries a child signed bundle=false,
+ %% the second a child signed bundle=true, the third has no nested
+ %% child at all. The L1 bundle TX therefore contains items that
+ %% would individually each round-trip with a different bundle state.
+ InnerFalse = hb_message:commit(
+ #{ <<"leaf-tag">> => <<"leaf-false">>,
+ <<"leaf-list">> => [1, 2, 3] },
+ TestOpts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => false }),
+ ?assert(hb_message:verify(InnerFalse, all, TestOpts)),
+ InnerTrue = hb_message:commit(
+ #{ <<"leaf-tag">> => <<"leaf-true">>,
+ <<"leaf-list">> => [4, 5, 6] },
+ TestOpts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => true }),
+ ?assert(hb_message:verify(InnerTrue, all, TestOpts)),
+ ItemA = hb_message:commit(
+ #{ <<"item-tag">> => <<"a">>, <<"inner">> => InnerFalse },
+ TestOpts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => true }),
+ ?assert(hb_message:verify(ItemA, all, TestOpts)),
+ ItemB = hb_message:commit(
+ #{ <<"item-tag">> => <<"b">>, <<"inner">> => InnerTrue },
+ TestOpts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => false }),
+ ?assert(hb_message:verify(ItemB, all, TestOpts)),
+ ItemC = hb_message:commit(
+ #{ <<"item-tag">> => <<"c">> },
+ TestOpts,
+ #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => false }),
+ ?assert(hb_message:verify(ItemC, all, TestOpts)),
+ {ok, SignedTX} = build_signed_tx([ItemA, ItemB, ItemC], TestOpts),
+ ?assert(ar_tx:verify(SignedTX)),
+ Committed = hb_message:convert(
+ SignedTX, <<"structured@1.0">>, <<"tx@1.0">>, TestOpts),
+ ?event(debug_test, {committed, {explicit, Committed}}),
+ ?assert(hb_message:verify(Committed, all, TestOpts)),
+ %% Convert back to TX (same path build_proofs uses) and check that
+ %% the data did not inflate.
+ TX = hb_message:convert(
+ Committed, <<"tx@1.0">>, <<"structured@1.0">>, TestOpts),
+ ?assert(ar_tx:verify(TX))
+ after
+ hb_mock_server:stop(ServerHandle)
+ end.
+
+%% Hardcoded item, structurally identical to one observed in a broken
+%% production bundle (TXID -BTiilFCWd2kB3oOdCpPDJLGXhjeNxIeMH3kerPXKCM).
+%% AO "Assignment" message with `body`, two commitments (HMAC + RSA-PSS),
+%% per-event commitments inside the body. All public key / signature
+%% bytes are real (from production) since the structured form encodes
+%% them.
+inlined_broken_item() ->
+ #{<<"base-hashpath">> =>
+ <<"w_l6KLmO8OeEM6vmdwX1HwdCDmHiOlhUyAeNdjwpspU/p4CQHPCo629uDl8seMpWN5Z4EZpRK6bUNPbGAoOIkrs">>,
+ <<"block-hash">> => <<"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA">>,
+ <<"block-height">> => 0,
+ <<"block-timestamp">> => 0,
+ <<"body">> =>
+ #{<<"commitments">> =>
+ #{<<"HkUAI3fWd3uHltdfyHzLU5IreUtmIIqv45ZxsC12psI">> =>
+ #{<<"commitment-device">> => <<"httpsig@1.0">>,
+ <<"committed">> =>
+ [<<"event">>, <<"reference">>, <<"status-class">>],
+ <<"committer">> =>
+ <<"mfj2T6f_3stKQk7fctrbpZKUfu4V6MQKCH-YHLtFnOY">>,
+ <<"keyid">> =>
+ <<"publickey:kvgGBmHtEOxZTuJuJuHVqBe51aLpSDvZrzj5RjNEbw8NrYm3GB9+BEKdYD+fHZ0H775PJf8mosGapkP6pB8h8ZEEc6AuOo+lTJ9SKEnYit1Q6YG5Dg306EDfm0dpMU3zKe9pE4CIHf3ffCDqa1Xh4c1zdcFqKyofeT8PWIGQZCScA8rYG+aG2Z/y6QduyxBgzFfITdzeXbnJONmZEwPEA29LeDWmCxA7CSfE6W8+2aDW75qQjETRVXzxou0I0tsc3uXzd9E0/yU6NbDi93sIBiO8z2pNbGrMGIfpH+4dirH/YZNBW8PBgOLjnpe5yoPrT+cI8OoEX27u/al/rkPG4u0wBlxnollSr/lXc5HIn6AWvpSF7nuXcmdtG8Y8RK95h4YZ9d6CG3tSOTvSk7wK8IH97hScB16EpiuT6Xi/TPYh3PwVC/VDxLMox19v1eP1riHC/nkIroerCmaIwGfxI2XNUgQzaTcygjT0DFbbLZFakCZpJ+0+u2/S1I4EbdpdcChWqrA8psUlyR3sbhhPDpEP1ldNO+08OyW/PMfMwXEkVR+WHM2t5m9cyZwjpes6epCQQWjMIkwqeIRZWwo607iJKsXgd5n+73FWytVWNS1mOH1nDDkfDXOq4R60B6C+2k0As14b2Dv4eXZstbr8KbtVIHit/IytBohieLpMcF0=">>,
+ <<"signature">> =>
+ <<"fZgshLexhVcpiQ1sBhwa_eoDn97vvc6IoJtwntc8VJTSDAokQ0RyThcjqhcgtF04kl4T986lZrVptAXkiKwog-gH1vnJX1T2yGAM-ZlTNTTmdLE7OIQhvs26-0_L3poPUSEjHsZ1vU2RpUUvKLIEQdCwlgTXGx54ZGB6feYXMn9e01tZPEdTVD0AcALa5G55aqyH3Lde5KXx4vOgdvWaCr772dXZ6C8249UG02SIHy3xvp1UdkLtzIbvSY9n5UzC1Bt-b5JftIijmVuIv3oI0_y9rRxGYLm2m7VusHwYjRdFAjN5X_NvYpWx25b62CNNLwprJfDqhllZsDz6PjnhRh9ZocOP3OLrarW0owFt0dfDRt3VBYaksUYTem-9YWtzS3Qa7kZSB754xtOW62wvu3kVH2sNB5C9SoXmheoPUjNLa4qXQv4-NJPF4wVdj8QxM0mYO0KQZfCUZtXhYYaqwRmS2aMyUrca1xjPOkD0nr7B1IS805O08fTkN6YcMluUH93myL4VbPPa2v1V2k-B-OlP4AzOn9F1uzk5ek--K_-2QdC63vgm4EKv8XqBoipUJ0Fe0jKUsE9iLZJddoMrYrsQCp8WMWX7iGaP6zJU2tbMpkAl-rr_Hc8xUkJ3eBd6pQcw-1MQ8EK7trPnjQD0EQZAG2HYj87HG-qCX3l9o8w">>,
+ <<"type">> => <<"rsa-pss-sha512">>},
+ <<"asDiK4CqvjJf2d9FFf3r3-xCrs1jA8ee9tWUp43BuWk">> =>
+ #{<<"commitment-device">> => <<"httpsig@1.0">>,
+ <<"committed">> =>
+ [<<"event">>, <<"reference">>, <<"status-class">>],
+ <<"keyid">> => <<"constant:ao">>,
+ <<"signature">> =>
+ <<"asDiK4CqvjJf2d9FFf3r3-xCrs1jA8ee9tWUp43BuWk">>,
+ <<"type">> => <<"hmac-sha256">>}},
+ <<"event">> => <<"is_admissible">>,
+ <<"reference">> =>
+ <<"HnbIWJdkG4CCwHCiycMKMmv2posdcTJ5xFcZ9lpTQQs">>,
+ <<"status-class">> => <<"success">>},
+ <<"commitments">> =>
+ #{<<"KT4ZXa_nhnWTfNJdVwOPDwHNN3eqYs_o3JoYv_odNvE">> =>
+ #{<<"commitment-device">> => <<"httpsig@1.0">>,
+ <<"committed">> =>
+ [<<"ao-types">>, <<"base-hashpath">>, <<"block-hash">>,
+ <<"block-height">>, <<"block-timestamp">>, <<"body">>,
+ <<"data-protocol">>, <<"epoch">>, <<"path">>,
+ <<"process">>, <<"slot">>, <<"timestamp">>, <<"type">>,
+ <<"variant">>],
+ <<"keyid">> => <<"constant:ao">>,
+ <<"signature">> =>
+ <<"KT4ZXa_nhnWTfNJdVwOPDwHNN3eqYs_o3JoYv_odNvE">>,
+ <<"type">> => <<"hmac-sha256">>},
+ <<"j1KSZD2tQOXpYvbPaqmLyRN6OOXxGa20bkgfeCj4a30">> =>
+ #{<<"bundle">> => <<"false">>,
+ <<"commitment-device">> => <<"ans104@1.0">>,
+ <<"committed">> =>
+ [<<"ao-types">>, <<"base-hashpath">>, <<"block-hash">>,
+ <<"block-height">>, <<"block-timestamp">>, <<"body">>,
+ <<"data-protocol">>, <<"epoch">>, <<"path">>,
+ <<"process">>, <<"slot">>, <<"timestamp">>, <<"type">>,
+ <<"variant">>],
+ <<"committer">> =>
+ <<"n_XZJhUnmldNFo4dhajoPZWhBXuJk-OcQr5JQ49c4Zo">>,
+ <<"keyid">> =>
+ <<"publickey:9BXuilimqVo7fpnoToPHZwqL7w_C0Qn4N3egeJRy05-nSpUv1vyp9xHbVLKVMPnJsie5Awt_xxob_jDvXSmE1fDsUpNnFurxG88UWN4zSNi87EfOorDQjHPRUqKPIYvg6xqPCpXPpOccJbFuack3ltQKtF5XLoaKWbsPdUtMquRXrbJgnGeOvXhQhbKa4xJKwGmjVC_LpY5FQ8j-cOlBOVVe_B7KF4eWG3sJf-z59MJQOaAozyU2iZpsuhslkTNVj8sM9CqkSfyD8EjEZdfF088IM_dJgk6ehIDHbx3FcGVnxpUHkXEnJFAlXRzdqmNb84QXsTNOHqwQPZ3q5wPRWS6iUaNxfeS_SsR6otIJgrYq04LYJLcpHuKGp53-b8tTeIvDFcmS2_kPijPqPINbf9c5uH0mxMNomB-8rVDIkIZ6Ojc_M0JnaQSk2rYPq8qRy2PuvAFyo1zeGM-2Bo4GNl9dMnfIr_Q6MlxRUwAwLHdOt0BJkxEBfOIw3MkB2d-SiVWtxG1Uqib7Iu_yn3j9DwzUOHjRQTse07giNDXRMsr1ml_sCK3bIetUFVnjjnoTNDEItDSck7lTFgvCdyXKkvXtSiNHkW8TCbdTDY0hBJzLVheKDb_cCyfmcTKo5ql2sWsZYCC7XybKdRMxU2HNNIUSpcDvhnTwv5-oq42Lmqc">>,
+ <<"signature">> =>
+ <<"sE9TuQTsMCHhaSOmHF-Wqu8QBbSNMSeSztiE1b0tJfmSNOe1nKPmMcCZN1rHD8L9xQWJw4hSVUbChwt4QReTz2IoXFz1NT80F1qCY2x3uFMFxgUHb2abTQW_-VNjFGWFe-sguwYLAIZGYoJ9a2g1EJCRfksk9iOWXRt7j_yIBixKATq-QsEWdcwfBsEUYWq-IRI1RdPAr9ToZeQ13TtWWYxcRbKHwxJ1M58p2CuLCi1OXVmENLjacAawuhBjGV4oTQ1-QBap-JOjB6kRTXtWjNGnMTPF01edFJIxgRncnODrTO_ehz6qkFH6iMhI9oV4w5VcRCKnNM7fxTXKj6DeiuAb1KrirpzohzsTLautMqRhst8gSViBlftd4XoVCDVscawuz8yPDyJoDxhIIup7mO51QSmNVTM6JpSEsG-CbXa64aECBOq7_x-ld9xHyNvCCSHetSJ3EBiJDWHE8XCurePGJ6GLeggugQ85LxgsRaLDm9UIlbMhopkK4X-SyXz5_pGwUSegLa1QHWWxnIaS5zTm0f4yi_YiBmgmS27v28T-nTzOHuBGTl8yUWVG_CKAELjFVREm5I7h4UuDQuFoXlkkFW22-Gyx5tZh1eSxRpl1NOwhyGc9O-6TIR46t1BhlItitOoi6JEf26JjTmwJWF7kR8xyahCYWtHFEkzpob4">>,
+ <<"type">> => <<"rsa-pss-sha256">>}},
+ <<"data-protocol">> => <<"ao">>,
+ <<"epoch">> => <<"0">>,
+ <<"path">> => <<"compute">>,
+ <<"process">> => <<"1V65_gzlifHH_surfFzL6HGfRlLJuEX_y0VbPHwIKec">>,
+ <<"slot">> => 180901,
+ <<"timestamp">> => 1778975170441,
+ <<"type">> => <<"Assignment">>,
+ <<"variant">> => <<"ao.N.1">>}.
diff --git a/src/preloaded/codec/dev_ans104.erl b/src/preloaded/codec/dev_ans104.erl
index 2b4b5facc..58584d345 100644
--- a/src/preloaded/codec/dev_ans104.erl
+++ b/src/preloaded/codec/dev_ans104.erl
@@ -2,7 +2,7 @@
%%% records to and from TABMs.
-module(dev_ans104).
-device_libraries([lib_arweave_common]).
--export([to/3, from/3, commit/3, verify/3, content_type/1]).
+-export([to/3, to_hint/3, from/3, commit/3, verify/3, content_type/1]).
-export([serialize/3, deserialize/3]).
-include("include/hb.hrl").
-include_lib("eunit/include/eunit.hrl").
@@ -128,6 +128,14 @@ do_from(RawTX, Req, Opts) ->
?event({from, {parsed_message, WithCommitments}}),
{ok, WithCommitments}.
+%% @doc Inspect a message's signed ans104 commitment and, if it carries an
+%% explicit `bundle' field, mirror that value onto the request `Req'.
+to_hint(Msg, Req, Opts) ->
+ case lib_arweave_common:bundle_hint(<<"ans104@1.0">>, Msg, Req, Opts) of
+ not_found -> {ok, Req};
+ Hint -> Hint
+ end.
+
%% @doc Internal helper to translate a message to its #tx record representation,
%% which can then be used by ar_bundles to serialize the message. We call the
%% message's device in order to get the keys that we will be checkpointing. We
@@ -144,55 +152,19 @@ to(Binary, _Req, _Opts) when is_binary(Binary) ->
}
};
to(TX, _Req, _Opts) when is_record(TX, tx) -> {ok, TX};
-to(RawTABM, Req, Opts) when is_map(RawTABM) ->
+to(TABM, Req, Opts) when is_map(TABM) ->
% Ensure that the TABM is fully loaded if the `bundle` key is set to true.
- ?event(ans104_to, {to, {inbound, RawTABM}, {req, Req}},
+ ?event(ans104_to, {to, {inbound, TABM}, {req, Req}},
#{debug_print_verify => false}),
- MaybeCommitment = hb_message:commitment(
- #{ <<"commitment-device">> => <<"ans104@1.0">> },
- RawTABM,
+ TX = lib_arweave_common:to(
+ <<"ans104@1.0">>, TABM, Req,
+ fun lib_arweave_common:fields_to_tx/4,
+ fun lib_arweave_common:excluded_tags/3,
Opts
),
- IsBundle = lib_arweave_common:is_bundle(MaybeCommitment, Req, Opts),
- MaybeBundle = lib_arweave_common:maybe_load(RawTABM, IsBundle, Opts),
- ?event(ans104_to, {to, {maybe_bundle, MaybeBundle}},
- #{debug_print_verify => false}),
-
- % Calculate and normalize the `data', if applicable.
- Data = lib_arweave_common:data(
- MaybeBundle, Req, fun dev_ans104:to/3, Opts),
- ?event(ans104_to, {to, {calculated_data, Data}},
- #{debug_print_verify => false}),
- TX0 = lib_arweave_common:siginfo(
- MaybeBundle, MaybeCommitment,
- fun lib_arweave_common:fields_to_tx/4, Opts
- ),
- ?event(ans104_to, {to, {found_siginfo, TX0}},
- #{debug_print_verify => false}),
- TX1 = TX0#tx { data = Data },
- % Calculate the tags for the TX.
- Tags = lib_arweave_common:tags(
- TX1, MaybeCommitment, MaybeBundle,
- lib_arweave_common:excluded_tags(TX1, MaybeBundle, Opts), Opts),
- ?event(ans104_to, {to, {calculated_tags, Tags}},
+ ?event(ans104_to, {to, {result, TX}},
#{debug_print_verify => false}),
- TX2 = TX1#tx { tags = Tags },
- Res =
- try ar_tx:normalize(TX2)
- catch
- Type:Error:Stacktrace ->
- ?event({
- {reset_ids_error, Error},
- {tx_without_data, {explicit, TX2}}}),
- ?event({prepared_tx_before_ids,
- {tags, {explicit, TX2#tx.tags}},
- {data, TX2#tx.data}
- }),
- erlang:raise(Type, Error, Stacktrace)
- end,
- ?event(ans104_to, {to, {result, Res}},
- #{debug_print_verify => false}),
- {ok, Res};
+ {ok, TX};
to(Other, _Req, _Opts) ->
throw({invalid_tx, Other}).
@@ -840,17 +812,18 @@ test_bundle_commitment(Commit, Encode, Decode) ->
hb_util:atom(hb_ao:get(<<"bundle">>, CommittedCommitment, false, Opts)),
Label),
- Encoded = hb_message:convert(Committed,
+ Encoded = hb_message:convert(Committed,
#{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => ToBool(Encode) },
- <<"structured@1.0">>, Opts),
+ <<"structured@1.0">>,
+ Opts),
?event(debug_test, {encoded, Label, {explicit, Encoded}}),
?assert(ar_bundles:verify_item(Encoded), Label),
%% IF the input message is unbundled, #tx.data should be empty.
?assertEqual(ToBool(Commit), Encoded#tx.data /= <<>>, Label),
- Decoded = hb_message:convert(Encoded,
+ Decoded = hb_message:convert(Encoded,
#{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => ToBool(Decode) },
- #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => ToBool(Encode) },
+ <<"ans104@1.0">>,
Opts),
?event(debug_test, {decoded, Label, {explicit, Decoded}}),
?assert(hb_message:verify(Decoded, all, Opts), Label),
@@ -883,16 +856,17 @@ test_bundle_uncommitted(Encode, Decode) ->
ToBool = fun(unbundled) -> false; (bundled) -> true end,
Label = lists:flatten(io_lib:format("~p -> ~p", [Encode, Decode])),
- Encoded = hb_message:convert(Structured,
+ Encoded = hb_message:convert(Structured,
#{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => ToBool(Encode) },
- <<"structured@1.0">>, Opts),
+ <<"structured@1.0">>,
+ Opts),
?event(debug_test, {encoded, Label, {explicit, Encoded}}),
%% IF the input message is unbundled, #tx.data should be empty.
?assertEqual(ToBool(Encode), Encoded#tx.data /= <<>>, Label),
- Decoded = hb_message:convert(Encoded,
+ Decoded = hb_message:convert(Encoded,
#{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => ToBool(Decode) },
- #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => ToBool(Encode) },
+ <<"ans104@1.0">>,
Opts),
?event(debug_test, {decoded, Label, {explicit, Decoded}}),
case Encode of
@@ -902,3 +876,4 @@ test_bundle_uncommitted(Encode, Decode) ->
?assertEqual([1, 2, 3], maps:get(<<"list">>, Decoded, Opts), Label)
end,
ok.
+
diff --git a/src/preloaded/codec/dev_structured.erl b/src/preloaded/codec/dev_structured.erl
index f93339031..8c20797bb 100644
--- a/src/preloaded/codec/dev_structured.erl
+++ b/src/preloaded/codec/dev_structured.erl
@@ -80,20 +80,30 @@ from(List, Req, Opts) when is_list(List) ->
{ok, hb_util:numbered_keys_to_list(DecodedAsMap, Opts)}
end;
from(Msg, Req, Opts) when is_map(Msg) ->
- % Normalize the message, offloading links to the cache.
- NormLinks = hb_link:normalize(Msg, linkify_mode(Req, Opts), Opts),
+ HintedReq = apply_bundle_hint(Msg, Req, Opts),
+ NormLinks = hb_link:normalize(Msg, linkify_mode(HintedReq, Opts), Opts),
NormKeysMap = hb_ao:normalize_keys(NormLinks, Opts),
- EncodeTypes = find_encode_types(Req, Opts),
+ EncodeTypes = find_encode_types(HintedReq, Opts),
{Types, Values} = lists:foldl(
fun (Key, {Types, Values}) ->
case hb_maps:find(Key, NormKeysMap, Opts) of
{ok, Value} when is_binary(Value) ->
{Types, [{Key, Value} | Values]};
- {ok, Nested} when is_map(Nested) or is_list(Nested) ->
+ {ok, Nested} when is_map(Nested) orelse is_list(Nested) ->
?event({from_recursing, {nested, Nested}}),
- {Types, [{Key, hb_util:ok(from(Nested, Req, Opts))} | Values]};
+ % We pass the HintedReq to the recursive call rather than
+ % Req so that this message's bundle status serves as the
+ % default for any children that don't explicitly set the
+ % `bundle' flag on the hinted commitment.
+ {Types,
+ [{
+ Key,
+ hb_util:ok(from(Nested, HintedReq, Opts))
+ } | Values]};
{ok, Value} when
- is_atom(Value) or is_integer(Value) or is_float(Value) ->
+ is_atom(Value)
+ orelse is_integer(Value)
+ orelse is_float(Value) ->
BinKey = hb_ao:normalize_key(Key),
?event({encode_value, Value}),
case maybe_encode_value(Value, EncodeTypes) of
@@ -136,7 +146,7 @@ from(Msg, Req, Opts) when is_map(Msg) ->
% Encode the AoTypes as a structured dictionary
% And include as a field on the produced TABM
WithTypes =
- hb_maps:from_list(case Types of
+ hb_maps:from_list(case Types of
[] -> Values;
T ->
AoTypes = iolist_to_binary(hb_structured_fields:dictionary(
@@ -174,16 +184,33 @@ type(Atom) when is_atom(Atom) -> <<"atom">>;
type(List) when is_list(List) -> <<"list">>;
type(Other) -> Other.
+%% @doc If a `hint-device` key is present it indicates the desired
+%% terminal format (after being converted via an intermediate `tabm`
+%% format). In that case dev_structured defers to the target codec
+%% to determine whether child messages should be loaded or unloaded.
+apply_bundle_hint(Msg, Req, Opts) ->
+ case hb_maps:get(<<"hint-device">>, Req, undefined, Opts) of
+ undefined -> Req;
+ DeviceBin ->
+ % May add a `bundle` key to the request
+ try hb_util:ok(
+ hb_ao:raw(DeviceBin, <<"to-hint">>, Msg, Req, Opts)
+ )
+ catch _:_ ->
+ Req
+ end
+ end.
+
%% @doc Discern the linkify mode from the request and the options.
linkify_mode(Req, Opts) ->
case hb_maps:get(<<"bundle">>, Req, not_found, Opts) of
- not_found -> hb_opts:get(linkify_mode, offload, Opts);
true ->
% The request is asking for a bundle, so we should _not_ linkify.
false;
- false ->
- % The request is asking for a flat message, so we should linkify.
- true
+ _ ->
+ % The request is either asking for a flat message or has not
+ % specified. In both cases we should linkify.
+ hb_opts:get(linkify_mode, offload, Opts)
end.
%% @doc Convert a TABM into a native HyperBEAM message.
diff --git a/src/preloaded/codec/dev_tx.erl b/src/preloaded/codec/dev_tx.erl
index b11a0c9a3..7e9ab9d0f 100644
--- a/src/preloaded/codec/dev_tx.erl
+++ b/src/preloaded/codec/dev_tx.erl
@@ -2,7 +2,7 @@
%%% records to and from TABMs.
-module(dev_tx).
-device_libraries([lib_arweave_common]).
--export([from/3, to/3, commit/3, verify/3]).
+-export([from/3, to/3, to_hint/3, commit/3, verify/3]).
-include("include/hb.hrl").
-include_lib("eunit/include/eunit.hrl").
@@ -102,6 +102,13 @@ do_from(RawTX, Req, Opts) ->
?event({from, {parsed_message, hb_util:human_id(TX#tx.id)}}),
{ok, WithCommitments}.
+%% @doc Inspect a message's signed tx@1.0 commitment and, if the commitment
+%% carries an explicit `bundle' field, mirror that value onto the request `Req'.
+to_hint(Msg, Req, Opts) ->
+ case lib_arweave_common:bundle_hint(<<"tx@1.0">>, Msg, Req, Opts) of
+ not_found -> hb_ao:raw(<<"ans104@1.0">>, <<"to-hint">>, Msg, Req, Opts);
+ Hint -> Hint
+ end.
%% @doc Internal helper to translate a message to its #tx record representation,
%% which can then be used by ar_tx to serialize the message. We call the
%% message's device in order to get the keys that we will be checkpointing. We
@@ -119,39 +126,17 @@ to(Binary, _Req, _Opts) when is_binary(Binary) ->
})
};
to(TX, _Req, _Opts) when is_record(TX, tx) -> {ok, TX};
-to(RawTABM, Req, Opts) when is_map(RawTABM) ->
- % Ensure that the TABM is fully loaded if the `bundle` key is set to true.
- ?event({to, {inbound, RawTABM}, {req, Req}}),
- MaybeCommitment = hb_message:commitment(
- #{ <<"commitment-device">> => <<"tx@1.0">> },
- RawTABM,
+to(TABM, Req, Opts) when is_map(TABM) ->
+ ?event({to, {inbound, TABM}, {req, Req}}),
+ TX = lib_arweave_common:to(
+ <<"tx@1.0">>, TABM, Req,
+ fun dev_tx_to:fields_to_tx/4,
+ fun dev_tx_to:excluded_tags/3,
Opts
),
- IsBundle = lib_arweave_common:is_bundle(MaybeCommitment, Req, Opts),
- MaybeBundle = lib_arweave_common:maybe_load(RawTABM, IsBundle, Opts),
- ?event({to, {raw_tabm, RawTABM}, {is_bundle, IsBundle}, {maybe_bundle, MaybeBundle}, {req, Req}, {opts, Opts}}),
- % Calculate and normalize the `data', if applicable.
- Data =
- lib_arweave_common:data(
- MaybeBundle, Req, fun lib_arweave_common:to/3, Opts),
- ?event({calculated_data, Data}),
- TX0 = lib_arweave_common:siginfo(
- MaybeBundle, MaybeCommitment,
- fun dev_tx_to:fields_to_tx/4, Opts),
- ?event({found_siginfo, TX0}),
- TX1 = TX0#tx { data = Data },
- % Calculate the tags for the TX.
- Tags = lib_arweave_common:tags(
- TX1, MaybeCommitment, MaybeBundle,
- dev_tx_to:excluded_tags(TX1, MaybeBundle, Opts),
- Opts),
- ?event({calculated_tags, Tags}),
- TX2 = TX1#tx { tags = Tags },
- ?event({tx_before_id_gen, TX2}),
- FinalTX = ar_tx:normalize(TX2),
- enforce_valid_tx(FinalTX),
- ?event({to_result, FinalTX}),
- {ok, FinalTX};
+ enforce_valid_tx(TX),
+ ?event({to_result, TX}),
+ {ok, TX};
to(Other, _Req, _Opts) ->
throw({invalid_tx, Other}).
@@ -1452,17 +1437,18 @@ test_bundle_commitment(Commit, Encode, Decode) ->
hb_util:atom(hb_ao:get(<<"bundle">>, CommittedCommitment, false, Opts)),
Label),
- Encoded = hb_message:convert(Committed,
+ Encoded = hb_message:convert(Committed,
#{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => ToBool(Encode) },
- <<"structured@1.0">>, Opts),
+ <<"structured@1.0">>,
+ Opts),
?event(debug_test, {encoded, Label, {explicit, Encoded}}),
?assert(ar_tx:verify(Encoded), Label),
%% IF the input message is unbundled, #tx.data should be empty.
?assertEqual(ToBool(Commit), Encoded#tx.data /= <<>>, Label),
- Decoded = hb_message:convert(Encoded,
+ Decoded = hb_message:convert(Encoded,
#{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => ToBool(Decode) },
- #{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => ToBool(Encode) },
+ <<"tx@1.0">>,
Opts),
?event(debug_test, {decoded, Label, {explicit, Decoded}}),
?assert(hb_message:verify(Decoded, all, Opts), Label),
@@ -1494,16 +1480,17 @@ test_bundle_uncommitted(Encode, Decode) ->
ToBool = fun(unbundled) -> false; (bundled) -> true end,
Label = lists:flatten(io_lib:format("~p -> ~p", [Encode, Decode])),
- Encoded = hb_message:convert(Structured,
+ Encoded = hb_message:convert(Structured,
#{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => ToBool(Encode) },
- <<"structured@1.0">>, Opts),
+ <<"structured@1.0">>,
+ Opts),
?event(debug_test, {encoded, Label, {explicit, Encoded}}),
- %% IF the input message is unbundled, #tx.data should be empty.
+ %% If the input message is unbundled, #tx.data should be empty.
?assertEqual(ToBool(Encode), Encoded#tx.data /= <<>>, Label),
- Decoded = hb_message:convert(Encoded,
+ Decoded = hb_message:convert(Encoded,
#{ <<"device">> => <<"structured@1.0">>, <<"bundle">> => ToBool(Decode) },
- #{ <<"device">> => <<"tx@1.0">>, <<"bundle">> => ToBool(Encode) },
+ <<"tx@1.0">>,
Opts),
?event(debug_test, {decoded, Label, {explicit, Decoded}}),
case Encode of
diff --git a/src/preloaded/codec/lib_arweave_common.erl b/src/preloaded/codec/lib_arweave_common.erl
index aa0951aea..755b9492c 100644
--- a/src/preloaded/codec/lib_arweave_common.erl
+++ b/src/preloaded/codec/lib_arweave_common.erl
@@ -1,10 +1,10 @@
%%% @doc Shared Arweave codec helpers.
-module(lib_arweave_common).
--export([from/3, to/3]).
+-export([from/3]).
-export([fields/3, tags/2, data/5, committed/6, base/5]).
-export([with_commitments/8]).
--export([is_bundle/3, maybe_load/3, data/4, tags/5, excluded_tags/3]).
--export([siginfo/4, fields_to_tx/4]).
+-export([bundle_hint/4, data/3, tags/5, excluded_tags/3]).
+-export([to/3, to/6, siginfo/4, fields_to_tx/4]).
-export([bundle_header/2, bundle_header/3]).
-include("include/hb.hrl").
@@ -34,35 +34,56 @@ from_item(RawTX, Req, Opts) ->
)
}.
-%% @doc Convert a message into its ANS-104 item form.
+%% @doc Recursively encode a nested message as an `ans104@1.0' #tx record.
to(Binary, _Req, _Opts) when is_binary(Binary) ->
{ok, #tx{ tags = [{<<"ao-type">>, <<"binary">>}], data = Binary }};
to(TX, _Req, _Opts) when is_record(TX, tx) ->
{ok, TX};
-to(RawTABM, Req, Opts) when is_map(RawTABM) ->
+to(TABM, Req, Opts) when is_map(TABM) ->
+ {ok,
+ to(
+ <<"ans104@1.0">>, TABM, Req,
+ fun ?MODULE:fields_to_tx/4,
+ fun ?MODULE:excluded_tags/3,
+ Opts
+ )};
+to(Other, _Req, _Opts) ->
+ throw({invalid_tx, Other}).
+
+to(Device, TABM, Req, FieldsFun, ExcludedTagsFun, Opts) ->
MaybeCommitment =
hb_message:commitment(
- #{ <<"commitment-device">> => <<"ans104@1.0">> },
- RawTABM,
+ #{ <<"commitment-device">> => Device },
+ TABM,
Opts
),
- IsBundle = is_bundle(MaybeCommitment, Req, Opts),
- MaybeBundle = maybe_load(RawTABM, IsBundle, Opts),
- Data = data(MaybeBundle, Req, fun lib_arweave_common:to/3, Opts),
- TX0 =
- siginfo(
- MaybeBundle, MaybeCommitment,
- fun lib_arweave_common:fields_to_tx/4, Opts
- ),
+ Data = data(TABM, Req, Opts),
+ ?event({calculated_data, Data}),
+ TX0 = siginfo(TABM, MaybeCommitment, FieldsFun, Opts),
+ ?event({found_siginfo, TX0}),
TX1 = TX0#tx{ data = Data },
- Tags =
- tags(
- TX1, MaybeCommitment, MaybeBundle,
- excluded_tags(TX1, MaybeBundle, Opts), Opts
- ),
- {ok, ar_tx:normalize(TX1#tx{ tags = Tags })};
-to(Other, _Req, _Opts) ->
- throw({invalid_tx, Other}).
+ Tags = tags(
+ TX1,
+ MaybeCommitment,
+ TABM,
+ ExcludedTagsFun(TX1, TABM, Opts),
+ Opts
+ ),
+ ?event({calculated_tags, Tags}),
+ TX = TX1#tx{ tags = Tags },
+ ?event({tx_before_id_gen, TX}),
+ try ar_tx:normalize(TX)
+ catch
+ Type:Error:Stacktrace ->
+ ?event({
+ {reset_ids_error, Error},
+ {tx_without_data, {explicit, TX}}}),
+ ?event({prepared_tx_before_ids,
+ {tags, {explicit, TX#tx.tags}},
+ {data, TX#tx.data}
+ }),
+ erlang:raise(Type, Error, Stacktrace)
+ end.
%% @doc Return a TABM message containing the fields of the given decoded
%% ANS-104 data item that should be included in the base message.
@@ -429,65 +450,24 @@ deduplicating_from_list(Tags, Opts) ->
%%% Encoding helpers.
-is_bundle({ok, _, Commitment}, _Req, Opts) ->
- hb_util:atom(hb_ao:get(<<"bundle">>, Commitment, false, Opts));
-is_bundle(_, Req, Opts) ->
- case hb_maps:is_key(<<"bundle">>, Req, Opts) of
- true -> hb_util:atom(hb_ao:get(<<"bundle">>, Req, false, Opts));
- false -> hb_util:atom(hb_ao:get(<<"bundle">>, Opts, false, Opts))
- end.
-
-%% @doc Determine if the message should be loaded from the cache and re-converted
-%% to the TABM format. We do this if the `bundle' key is set to true.
-maybe_load(RawTABM, true, Opts) ->
- % Convert back to the fully loaded structured@1.0 message, then
- % convert to TABM with bundling enabled.
- Structured = hb_message:convert(RawTABM, <<"structured@1.0">>, Opts),
- Loaded = hb_cache:ensure_all_loaded(Structured, Opts),
- % Convert to TABM with bundling enabled.
- LoadedTABM =
- hb_message:convert(
- Loaded,
- tabm,
+%% @doc Apply the `bundle' hint from a signed commitment for `Device'.
+%% Returns `not_found' when no signed commitment for `Device' exists.
+bundle_hint(Device, Msg, Req, Opts) ->
+ case hb_message:commitment(
#{
- <<"device">> => <<"structured@1.0">>,
- <<"bundle">> => true
+ <<"commitment-device">> => Device,
+ <<"committer">> => '_'
},
- Opts
- ),
- % Ensure the commitments from the original message are the only
- % ones in the fully loaded message, recursively for nested maps.
- replace_commitments_recursive(LoadedTABM, RawTABM);
-maybe_load(RawTABM, false, _Opts) ->
- RawTABM.
-
-%% @doc Recursively replace commitments from RawTABM into LoadedTABM.
-replace_commitments_recursive(LoadedTABM, RawTABM)
- when is_map(LoadedTABM), is_map(RawTABM) ->
- LoadedTABM2 =
- case maps:find(<<"commitments">>, RawTABM) of
- {ok, RawCommitments} ->
- LoadedTABM#{ <<"commitments">> => RawCommitments };
- error ->
- maps:remove(<<"commitments">>, LoadedTABM)
- end,
- maps:map(
- fun(<<"commitments">>, Value) ->
- Value;
- (Key, Value) when is_map(Value) ->
- case maps:get(Key, RawTABM, undefined) of
- RawValue when is_map(RawValue) ->
- replace_commitments_recursive(Value, RawValue);
- _ ->
- Value
+ Msg,
+ Opts) of
+ {ok, _, Commitment} ->
+ case hb_util:atom(
+ hb_maps:get(<<"bundle">>, Commitment, not_found, Opts)) of
+ not_found -> {ok, Req};
+ Value -> {ok, Req#{ <<"bundle">> => Value }}
end;
- (_Key, Value) ->
- Value
- end,
- LoadedTABM2
- );
-replace_commitments_recursive(LoadedTABM, _RawTABM) ->
- LoadedTABM.
+ _ -> not_found
+ end.
%% @doc Calculate the fields for a message, returning an initial TX record.
siginfo(_Message, {ok, _, Commitment}, FieldsFun, Opts) ->
@@ -568,13 +548,13 @@ fields_to_tx(TX, Prefix, Map, Opts) ->
}.
%% @doc Calculate the data field for a message.
-data(TABM, Req, ToFun, Opts) ->
+data(TABM, Req, Opts) ->
DataKey = inline_key(TABM),
UnencodedNestedMsgs = data_messages(TABM, Opts),
NestedMsgs =
hb_maps:map(
fun(_, Msg) ->
- hb_util:ok(ToFun(Msg, Req, Opts))
+ hb_util:ok(to(Msg, Req, Opts))
end,
UnencodedNestedMsgs,
Opts
@@ -587,7 +567,7 @@ data(TABM, Req, ToFun, Opts) ->
{?DEFAULT_DATA, _} ->
NestedMsgs;
{DataVal, _} ->
- NestedMsgs#{ DataKey => hb_util:ok(ToFun(DataVal, Req, Opts)) }
+ NestedMsgs#{ DataKey => hb_util:ok(to(DataVal, Req, Opts)) }
end.
%% @doc Calculate data messages for large tag values or nested messages.
diff --git a/src/preloaded/message/dev_message.erl b/src/preloaded/message/dev_message.erl
index 837e974a9..f158e9bb9 100644
--- a/src/preloaded/message/dev_message.erl
+++ b/src/preloaded/message/dev_message.erl
@@ -135,21 +135,36 @@ id(RawBase, Req, NodeOpts) ->
end.
calculate_id(RawBase, Req, NodeOpts) ->
- % Find the ID device for the message.
- Base = hb_message:convert(RawBase, tabm, NodeOpts),
- ?event(debug_id, {calculate_ids, {base, Base}}),
- IDMod =
- case id_device(Base, NodeOpts) of
- {ok, IDDev} -> IDDev;
+ % Resolve the ID device up-front so we can plumb it as `hint-device' into
+ % the structured->tabm conversion below. This keeps the children's load
+ % state consistent with what `commit/3' and `verify/3' would produce.
+ IDDev =
+ case id_device(RawBase, NodeOpts) of
+ {ok, Device} -> Device;
{error, Error} -> throw({id, Error})
end,
- ?event(debug_id, {generating_id, {idmod, IDMod}, {base, Base}}),
- % If the ID device resolves to this device, use the default commitment
- % device instead to avoid recursing through `message@1.0/commit'.
+ % Encode to a TABM. The `bundle' flag (when set on the request) is the
+ % caller's intent for the top-level message and applies only to the root;
+ % `hint-device' lets the structured codec reproduce each nested
+ % commitment's own bundle state per-node, so the id is computed over the
+ % same shape `commit/3' and `verify/3' would produce.
+ SourceSpec =
+ hb_message:add_bundle_hint(
+ #{ <<"device">> => <<"structured@1.0">> },
+ Req#{ <<"device">> => IDDev },
+ NodeOpts
+ ),
+ Base = hb_message:convert(RawBase, tabm, SourceSpec, NodeOpts),
+ ?event(debug_id, {calculate_ids, {base, Base}}),
+ ?event(debug_id, {generating_id, {id_device, IDDev}, {base, Base}}),
+ % Get the commitment device name from the message, or use the default if
+ % it is not set. We can tell if the device is not set (or is the default)
+ % by checking whether the resolved device module is this module itself.
+ % `hb_ao:raw/5' expects a device name, not a resolved module.
CommitDev =
- case hb_device:message_to_device(#{ <<"device">> => IDMod }, NodeOpts) of
+ case hb_device:message_to_device(#{ <<"device">> => IDDev }, NodeOpts) of
?MODULE -> ?DEFAULT_ID_DEVICE;
- _ -> IDMod
+ _ -> IDDev
end,
?event(debug_id, {called_id_device, CommitDev}, NodeOpts),
{ok, #{ <<"commitments">> := Comms} } =
@@ -246,10 +261,19 @@ commit(Self, Req, Opts) ->
_ ->
Opts#{ <<"linkify-mode">> => offload }
end,
- % Encode to a TABM
+ % Encode to a TABM. The `bundle' flag (when set on the request) is the
+ % caller's intent for the top-level commit and applies only to the root
+ % message; `hint-device' lets the structured codec preserve each nested
+ % commitment's own bundle state per-node.
+ SourceSpec =
+ hb_message:add_bundle_hint(
+ #{ <<"device">> => <<"structured@1.0">> },
+ Req#{ <<"device">> => AttDev },
+ CommitOpts
+ ),
Loaded =
ensure_commitments_loaded(
- hb_message:convert(Base, tabm, CommitOpts),
+ hb_message:convert(Base, tabm, SourceSpec, CommitOpts),
Opts
),
{ok, Committed} =
@@ -269,22 +293,13 @@ commit(Self, Req, Opts) ->
verify(Self, Req, Opts) ->
% Get the target message of the verification request.
{ok, RawBase} = hb_message:find_target(Self, Req, Opts),
- Base =
- hb_message:convert(
- ensure_commitments_loaded(
- RawBase,
- Opts
- ),
- tabm,
- Opts
- ),
- ?event(verify, {verify, {base_found, Base}}),
- Commitments = maps:get(<<"commitments">>, Base, #{}),
- IDsToVerify = commitment_ids_from_request(Base, Req, Opts),
+ CommitmentBase = ensure_commitments_loaded(RawBase, Opts),
+ Commitments = maps:get(<<"commitments">>, CommitmentBase, #{}),
+ IDsToVerify = commitment_ids_from_request(CommitmentBase, Req, Opts),
% Generate the new commitment request base messsage by removing the keys
% used by this function (path, committers, commitments) and returning the
% remaining keys. This message will then be merged with each commitment
- % message to generate the final request, allowing the caller to pass
+ % message to generate the final request, allowing the caller to pass
% additional keys to the commitment device.
ReqBase =
maps:without(
@@ -300,13 +315,36 @@ verify(Self, Req, Opts) ->
Res =
lists:all(
fun(CommitmentID) ->
+ Commitment = maps:merge(
+ ReqBase,
+ maps:get(CommitmentID, Commitments)
+ ),
+ % Build the source spec from the commitment device alone: a
+ % `hint-device' lets the structured codec reproduce each
+ % subtree in the bundle state it was committed in. The verify
+ % request's `bundle' is deliberately *not* propagated -- a
+ % commitment is always verified in the state it was signed
+ % in, so any `bundle' passed by the caller is irrelevant.
+ SourceSpec =
+ hb_message:add_bundle_hint(
+ #{ <<"device">> => <<"structured@1.0">> },
+ #{
+ <<"device">> =>
+ maps:get(
+ <<"commitment-device">>,
+ Commitment,
+ undefined
+ )
+ },
+ Opts
+ ),
+ Base = hb_message:convert(
+ CommitmentBase, tabm, SourceSpec, Opts),
+ ?event(verify, {verify, {base_found, Base}}),
{ok, Res} =
verify_commitment(
Base,
- maps:merge(
- ReqBase,
- maps:get(CommitmentID, Commitments)
- ),
+ Commitment,
Opts
),
?event(verify,
diff --git a/test/arbundles.js/upload-dataitem.js b/test/arbundles.js/upload-dataitem.js
index 7f153d334..1175a5faf 100644
--- a/test/arbundles.js/upload-dataitem.js
+++ b/test/arbundles.js/upload-dataitem.js
@@ -45,7 +45,7 @@ async function uploadDataItem(itemPath, gatewayUrl = "https://up.arweave.net") {
// Upload to the gateway
console.log("Uploading to gateway...");
- const uploadUrl = `${gatewayUrl}/tx`;
+ const uploadUrl = `${gatewayUrl}/~bundler@1.0/tx`;
const response = await axios.post(uploadUrl, itemBuffer, {
headers: {
diff --git a/test/arbundles.js/upload-items.js b/test/arbundles.js/upload-items.js
index 9bc63d475..67579202f 100644
--- a/test/arbundles.js/upload-items.js
+++ b/test/arbundles.js/upload-items.js
@@ -3,19 +3,20 @@ const path = require("path");
const { ArweaveSigner, createData } = require("@dha-team/arbundles");
// Configuration
-const BUNDLER_URL = "http://localhost:8734";
+const BUNDLER_URL = process.env.BUNDLER_URL || "http://localhost:8734";
+const ENDPOINT_PATH = process.env.ENDPOINT_PATH || "/~bundler@1.0/item?codec-device=ans104@1.0";
const DEFAULT_WALLET = "../../hyperbeam-key.json";
const CONCURRENT_UPLOADS = 100; // Number of parallel uploads
async function performanceTest(walletPath, itemCount, bytesPerItem = 0) {
const wallet = require(path.resolve(walletPath));
const signer = new ArweaveSigner(wallet);
- const endpoint = `${BUNDLER_URL}/~bundler@1.0/item?codec-device=ans104@1.0`;
+ const endpoint = `${BUNDLER_URL}${ENDPOINT_PATH}`;
console.log("\n" + "=".repeat(70));
console.log("ANS-104 Bundle Upload Performance Test");
console.log("=".repeat(70));
- console.log(`Target: ${BUNDLER_URL}`);
+ console.log(`Target: ${endpoint}`);
console.log(`Items: ${itemCount}`);
console.log(`Item Size: ${bytesPerItem > 0 ? `~${bytesPerItem} bytes` : 'default'}`);
console.log(`Concurrent: ${CONCURRENT_UPLOADS}`);
@@ -147,10 +148,20 @@ if (require.main === module) {
console.error(" number_of_items - Number of data items to create and upload");
console.error(" bytes_per_item - Minimum size of each item in bytes (optional)");
console.error("");
+ console.error("Environment variables:");
+ console.error(" BUNDLER_URL - Gateway base URL (default: http://localhost:8734)");
+ console.error(" ENDPOINT_PATH - Path appended to gateway (default: /~bundler@1.0/item?codec-device=ans104@1.0)");
+ console.error("");
console.error("Examples:");
console.error(" node upload-items.js 100");
console.error(" node upload-items.js 100 1024");
console.error(" node upload-items.js /path/to/wallet.json 100 1024");
+ console.error(" BUNDLER_URL=https://forward.computer node upload-items.js 100");
+ console.error(" BUNDLER_URL=https://forward.computer ENDPOINT_PATH='/~bundler@1.0/tx?codec-device=ans104@1.0' node upload-items.js 1");
+ console.error("");
+ console.error("Note: when posting raw ANS-104 bytes, ENDPOINT_PATH must include");
+ console.error(" ?codec-device=ans104@1.0 — otherwise the server will reject");
+ console.error(" the body as 'unsigned-item' (no signers visible).");
process.exit(1);
}