Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions src/core/resolver/hb_cache.erl
Original file line number Diff line number Diff line change
Expand Up @@ -710,9 +710,10 @@ read_all_commitments(Msg, Opts) ->
true,
{
CommitmentID,
ensure_all_loaded(
load_commitment(
Commitment,
Opts#{ <<"commitment">> => true }
UncommittedID,
Opts
)
}
};
Expand Down Expand Up @@ -866,9 +867,10 @@ prepare_typed_values(Target, RootPath, Subpaths, Values, Store, Opts) ->
case do_read_commitment(CommPath, Opts) of
{ok, Commitment} ->
LoadedCommitment =
ensure_all_loaded(
load_commitment(
Commitment,
Opts#{ <<"commitment">> => true }
RootPath,
Opts
),
?event(read_commitment,
{found_target_commitment,
Expand Down Expand Up @@ -962,6 +964,34 @@ prepare_typed_values(Target, RootPath, Subpaths, Values, Store, Opts) ->
end
end.

%% @doc Fully load a commitment while recovering the legacy representation of

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a legacy representation here? If there is a better representation at this point we should just cleanly implement that. Always fight the agent introducing legacy pathways where not strictly necessary.

%% an empty `committed' key list. Lua historically decoded that empty list as
%% `#{}`. Its cache ID is also the uncommitted ID of an empty message, so loading
%% the link from a commitment stored at that same root recursively re-entered
%% the root forever. A first-hop link back to the commitment's base therefore
%% represents the empty committed-key list.
load_commitment(Commitment, BasePath, Opts) ->
CommitmentOpts = Opts#{ <<"commitment">> => true },
case maps:get(<<"committed">>, Commitment, not_found) of
{link, LinkID, LinkOpts = #{
<<"type">> := <<"link">>,
<<"lazy">> := true
}} ->
LinkReadOpts =
hb_util:deep_merge(CommitmentOpts, LinkOpts, CommitmentOpts),
case do_read_commitment(LinkID, LinkReadOpts) of
{ok, BasePath} ->
ensure_all_loaded(
Commitment#{ <<"committed">> => [] },
CommitmentOpts
);
_ ->
ensure_all_loaded(Commitment, CommitmentOpts)
end;
_ ->
ensure_all_loaded(Commitment, CommitmentOpts)
end.

apply_type_to_immediate(Value, #{ <<"type">> := Type }) ->
hb_util:decode(Type, Value);
apply_type_to_immediate(Value, _LinkOpts) ->
Expand Down Expand Up @@ -1156,6 +1186,33 @@ test_store_unsigned_empty_message(Store) ->
?event_debug({match_result, MatchRes}),
?assert(MatchRes).

test_store_lua_decoded_empty_commitment(Store) ->
?event_debug(debug_store_test, {store, Store}),
hb_store:reset(Store),
Opts = #{ <<"store">> => Store },
SignedEmpty =
hb_message:commit(
#{},
Opts,
#{ <<"type">> => <<"unsigned">> }
),
% Lua historically decoded the empty committed-key list as an empty map.
% Recreate that persisted representation to ensure it remains readable.
LuaDecoded =
SignedEmpty#{
<<"commitments">> =>
maps:map(
fun(_ID, Commitment) ->
Commitment#{ <<"committed">> => #{} }
end,
maps:get(<<"commitments">>, SignedEmpty)
)
},
{ok, Path} = write(LuaDecoded, Opts),
{ok, Retrieved} = read(Path, Opts),
[Commitment] = maps:values(maps:get(<<"commitments">>, Retrieved)),
?assertEqual([], maps:get(<<"committed">>, Commitment)).

test_store_unsigned_nested_empty_message(Store) ->
?event_debug(debug_store_test, {store, Store}),
hb_store:reset(Store),
Expand Down Expand Up @@ -1444,6 +1501,8 @@ cache_suite_test_() ->
hb_store:generate_test_suite([
{"store unsigned empty message",
fun test_store_unsigned_empty_message/1},
{"store Lua-decoded empty commitment",
fun test_store_lua_decoded_empty_commitment/1},
{"store binary", fun test_store_binary/1},
{"store unsigned nested empty message",
fun test_store_unsigned_nested_empty_message/1},
Expand Down
82 changes: 65 additions & 17 deletions src/preloaded/vm/dev_lua.erl
Original file line number Diff line number Diff line change
Expand Up @@ -315,20 +315,18 @@ compute(Key, RawBase, RawReq, Opts) ->
Opts#{ <<"hashpath">> => ignore }
),
?event(debug_lua, parameters_found),
% Resolve all hyperstate links
ResolvedParams = hb_cache:ensure_all_loaded(Params, Opts),
% Call the VM function with the given arguments.
?event(lua,
{calling_lua_func,
{function, Function},
{args, ResolvedParams},
{args, Params},
{req, Req}
}
),
process_response(
try luerl:call_function_dec(
[Function],
encode(ResolvedParams, Opts),
encode(Params, Opts),
State
)
catch
Expand Down Expand Up @@ -425,7 +423,11 @@ normalize(Base, _Req, RawOpts) ->

%% @doc Decode a Lua result into a HyperBEAM `structured@1.0' message.
decode(EncMsg, Opts) ->
hb_message:normalize_commitments(do_decode(EncMsg, Opts), Opts, verify).
hb_message:normalize_commitments(
normalize_decoded_commitments(do_decode(EncMsg, Opts)),
Opts,
verify
).
do_decode(EncMsg, _Opts) when is_list(EncMsg) andalso length(EncMsg) == 0 ->
% The value is an empty table, so we assume it is a message rather than
% a list.
Expand All @@ -452,22 +454,52 @@ do_decode(Msg, Opts) when is_map(Msg) ->
do_decode(Other, _Opts) ->
Other.

%% @doc Restore the list type of commitment key sets after crossing the Lua
%% boundary. Lua has only one table type, so an empty list is decoded as an
%% empty message by `do_decode/2'. HTTPSig commitments define `committed' as a
%% list; leaving an empty value as `#{}` makes it share the empty-message cache
%% ID and can create a self-referential cache entry.
normalize_decoded_commitments(Msg) when is_map(Msg) ->
maps:map(
fun(<<"commitments">>, Commitments) when is_map(Commitments) ->
maps:map(
fun(_ID, Commitment) ->
normalize_decoded_commitment(Commitment)
end,
Commitments
);
(_Key, Value) ->
normalize_decoded_commitments(Value)
end,
Msg
);
normalize_decoded_commitments(List) when is_list(List) ->
lists:map(fun normalize_decoded_commitments/1, List);
normalize_decoded_commitments(Value) ->
Value.

normalize_decoded_commitment(Commitment) when is_map(Commitment) ->
case maps:get(<<"committed">>, Commitment, not_found) of
Committed when is_map(Committed), ?IS_EMPTY_MESSAGE(Committed) ->
Commitment#{ <<"committed">> => [] };
_ ->
Commitment
end;
normalize_decoded_commitment(Commitment) ->
Commitment.

%% @doc Encode a HyperBEAM `structured@1.0' message into a Lua term.
encode(Map, Opts) ->
hb_message:normalize_commitments(do_encode(Map, Opts), Opts).
do_encode(Map, Opts).
do_encode(Map, Opts) when is_map(Map) ->
hb_cache:ensure_all_loaded(
case hb_util:is_ordered_list(Map, Opts) of
true -> do_encode(hb_util:message_to_ordered_list(Map), Opts);
false -> maps:to_list(maps:map(fun(_, V) -> do_encode(V, Opts) end, Map))
end,
Opts
);
case hb_util:is_ordered_list(Map, Opts) of
true -> do_encode(hb_util:message_to_ordered_list(Map), Opts);
false -> maps:to_list(maps:map(fun(_, V) -> do_encode(V, Opts) end, Map))
end;
do_encode(List, Opts) when is_list(List) ->
hb_cache:ensure_all_loaded(
lists:map(fun(V) -> do_encode(V, Opts) end, List),
Opts
);
lists:map(fun(V) -> do_encode(V, Opts) end, List);
do_encode(Link, Opts) when ?IS_LINK(Link) ->
do_encode(hb_cache:ensure_all_loaded(Link, Opts), Opts);
do_encode(Atom, _Opts) when is_atom(Atom) and (Atom /= false) and (Atom /= true)->
hb_util:bin(Atom);
do_encode(Other, _Opts) ->
Expand Down Expand Up @@ -525,6 +557,22 @@ simple_invocation_test() ->
},
?assertEqual(2, hb_ao:get(<<"assoctable/b">>, Base, #{})).

empty_commitment_key_list_decode_test() ->
CommitmentID = <<"test-commitment">>,
Decoded = normalize_decoded_commitments(#{
<<"commitments">> => #{
CommitmentID => #{ <<"committed">> => #{} }
}
}),
?assertEqual(
[],
hb_ao:get(
<<"commitments/test-commitment/committed">>,
Decoded,
#{}
)
).

post_invocation_message_validation_test() ->
{ok, Script} = file:read_file("test/test.lua"),
Opts = #{ <<"priv-wallet">> => hb:wallet() },
Expand Down