diff --git a/src/dev_json_iface.erl b/src/dev_json_iface.erl index a78209df..66433ab1 100644 --- a/src/dev_json_iface.erl +++ b/src/dev_json_iface.erl @@ -1,25 +1,23 @@ %%% @doc A device that provides a way for WASM execution to interact with %%% the HyperBEAM (and AO) systems, using JSON as a shared data representation. %%% -%%% The interface is extremely basic. It works as follows: +%%% The interface is easy to use. It works as follows: %%% %%% 1. The device is given a message that contains a process definition, WASM %%% environment, and a message that contains the data to be processed, %%% including the image to be used in part of `execute{pass=1}'. %%% 2. The device is called with `execute{pass=2}', which reads the result of -%%% the process execution from the WASM environment and returns it as a +%%% the process execution from the WASM environment and adds it to the %%% message. %%% %%% The device has the following requirements and interface: %%% ``` -%%% M1/Computed when M2/Pass == 1 -> +%%% M1/Computed when /Pass == 1 -> %%% Assumes: %%% M1/priv/WASM/Port %%% M1/Process %%% M2/Message -%%% M2/Message/Image -%%% M2/Image -%%% M2/Block-Height +%%% M2/Assignment/Block-Height %%% Generates: %%% /WASM/Handler %%% /WASM/Params @@ -37,13 +35,17 @@ %%% /Results/Data''' -module(dev_json_iface). --export([computed/3]). +-export([init/3, compute/3]). -include("include/hb.hrl"). -include_lib("eunit/include/eunit.hrl"). +%% @doc Initialize the device. +init(M1, _M2, _Opts) -> + {ok, hb_converge:set(M1, #{<<"WASM-Function">> => <<"handle">>})}. + %% @doc On first pass prepare the call, on second pass get the results. -computed(M1, M2, Opts) -> - case hb_converge:get(pass, M1, M2) of +compute(M1, M2, Opts) -> + case hb_converge:get(<<"Pass">>, M1, Opts) of 1 -> prep_call(M1, M2, Opts); 2 -> results(M1, M2, Opts); _ -> {ok, M1} @@ -52,15 +54,17 @@ computed(M1, M2, Opts) -> %% @doc Prepare the WASM environment for execution by writing the process string and %% the message as JSON representations into the WASM environment. prep_call(M1, M2, Opts) -> - Port = hb_converge:get(<<"priv/WASM/Port">>, M1, Opts), - Process = hb_converge:get(<<"Process">>, M1, Opts), - Assignment = hb_converge:get(<<"Assignment">>, M2, Opts), - Message = hb_converge:get(<<"Message">>, M2, Opts), - Image = hb_converge:get(<<"Image">>, Message, Opts), + Port = hb_private:get(<<"priv/WASM/Port">>, M1, Opts), + Process = hb_converge:get(<<"Process">>, M1, Opts, #{ hashpath => ignore }), + Assignment = hb_converge:get(<<"Assignment">>, M2, Opts#{ hashpath => ignore }), + Message = hb_converge:get(<<"Message">>, M2, Opts#{ hashpath => ignore }), + Image = hb_converge:get(<<"Process/WASM-Image">>, M1, Opts), BlockHeight = hb_converge:get(<<"Block-Height">>, Assignment, Opts), RawMsgJson = - ar_bundles:item_to_json_struct(hb_message:message_to_tx(Message)), - {Props} = jiffy:decode(RawMsgJson), + ar_bundles:item_to_json_struct( + hb_message:message_to_tx(Message) + ), + {Props} = RawMsgJson, MsgJson = jiffy:encode({ Props ++ [ @@ -86,8 +90,8 @@ prep_call(M1, M2, Opts) -> hb_converge:set( M1, #{ - <<"WASM/Handler">> => <<"handle">>, - <<"WASM/Params">> => [MsgJsonPtr, ProcessJsonPtr] + <<"WASM-Function">> => <<"handle">>, + <<"WASM-Params">> => [MsgJsonPtr, ProcessJsonPtr] }, Opts ) @@ -96,10 +100,10 @@ prep_call(M1, M2, Opts) -> %% @doc Read the computed results out of the WASM environment, assuming that %% the environment has been set up by `prep_call/3' and that the WASM executor %% has been called with `computed{pass=1}'. -results(M1, M2, Opts) -> - Port = hb_converge:get(<<"priv/WASM/Port">>, M1, Opts), - Type = hb_converge:get(<<"Results/WASM/Type">>, M2, Opts), - Proc = hb_converge:get(<<"Process">>, M2, Opts), +results(M1, _M2, Opts) -> + Port = hb_private:get(<<"priv/WASM/Port">>, M1, Opts), + Type = hb_converge:get(<<"Results/WASM/Type">>, M1, Opts), + Proc = hb_converge:get(<<"Process">>, M1, Opts), case hb_converge:to_key(Type) of error -> {error, @@ -116,9 +120,8 @@ results(M1, M2, Opts) -> ) }; ok -> - [Ptr] = hb_converge:get(<<"WASM/Results/Body">>, M1, Opts), + [Ptr] = hb_converge:get(<<"Results/WASM/Output">>, M1, Opts), {ok, Str} = hb_beamr_io:read_string(Port, Ptr), - Wallet = hb_opts:get(wallet, no_viable_wallet, Opts), try jiffy:decode(Str, [return_maps]) of #{<<"ok">> := true, <<"response">> := Resp} -> % TODO: Handle all JSON interface output types. @@ -126,57 +129,116 @@ results(M1, M2, Opts) -> <<"Output">> := #{<<"data">> := Data}, <<"Messages">> := Messages } = Resp, - hb_converge:set( - M1, - #{ - <<"Results/Outbox">> => - [ - { - list_to_binary(integer_to_list(MessageNum)), - ar_bundles:sign_item( - ar_bundles:json_struct_to_item( - preprocess_results(Msg, Proc, Opts) - ), - Wallet - ) - } - || - {MessageNum, Msg} <- - lists:zip(lists:seq(1, length(Messages)), Messages) - ], - <<"Results/Data">> => Data - }, - Opts - ) + Res = + hb_converge:set( + M1, + #{ + <<"Results/Outbox">> => + maps:from_list([ + {MessageNum, preprocess_results(Msg, Proc, Opts)} + || + {MessageNum, Msg} <- + lists:zip(lists:seq(1, length(Messages)), Messages) + ]), + <<"Results/Data">> => Data + }, + Opts + ), + {ok, Res} catch _:_ -> - hb_converge:set( - M1, - #{ - <<"Results/Outbox">> => undefined, - <<"Results/Body">> => <<"JSON error parsing WASM result output.">> - }, - Opts - ) + {error, + hb_converge:set( + M1, + #{ + <<"Results/Outbox">> => undefined, + <<"Results/Body">> => <<"JSON error parsing WASM result output.">> + }, + Opts + ) + } end end. -%% NOTE: After the process returns messages from an evaluation, the signing unit needs to add -%% some tags to each message, and spawn to help the target process know these messages are created -%% by a process. +%% @doc After the process returns messages from an evaluation, the +%% signing node needs to add some tags to each message and spawn such that +%% the target process knows these messages are created by a process. preprocess_results(Msg, Proc, Opts) -> - {ok, FilteredMsg} = - hb_converge:resolve(Msg, - #{ - path => remove, - items => [<<"From-Process">>, <<"From-Image">>, <<"Anchor">>] - }, - Opts + RawTags = maps:get(<<"Tags">>, Msg, []), + TagList = + [ + {maps:get(<<"name">>, Tag), maps:get(<<"value">>, Tag)} + || + Tag <- RawTags ], + Tags = maps:from_list(TagList), + FilteredMsg = + maps:without( + [<<"From-Process">>, <<"From-Image">>, <<"Anchor">>, <<"Tags">>], + Msg ), - hb_converge:set( + maps:merge( FilteredMsg, - #{ + Tags#{ <<"From-Process">> => hb_util:id(Proc, signed), - <<"From-Image">> => element(2, lists:keyfind(<<"Image">>, 1, Proc#tx.tags)) + <<"From-Image">> => hb_converge:get(<<"WASM-Image">>, Proc, Opts) } ). + +%%% Tests + +test_init() -> + application:ensure_all_started(hb). + +generate_stack(File) -> + test_init(), + Wallet = hb:wallet(), + Msg0 = dev_wasm:store_wasm_image(File), + Image = maps:get(<<"WASM-Image">>, Msg0), + Msg1 = Msg0#{ + device => <<"Stack/1.0">>, + <<"Device-Stack">> => + [ + <<"WASI/1.0">>, + <<"JSON-Iface/1.0">>, + <<"WASM-64/1.0">>, + <<"Multipass/1.0">> + ], + <<"Passes">> => 2, + <<"Stack-Keys">> => [<<"Init">>, <<"Compute">>], + <<"Process">> => + hb_message:sign(#{ + <<"Type">> => <<"Process">>, + <<"WASM-Image">> => Image, + <<"Scheduler">> => hb:address(), + <<"Authority">> => hb:address() + }, Wallet) + }, + {ok, Msg2} = hb_converge:resolve(Msg1, <<"Init">>, #{}), + Msg2. + +generate_aos_msg(ProcID, Code) -> + Wallet = hb:wallet(), + #{ + path => <<"Compute">>, + <<"Message">> => + hb_message:sign(#{ + <<"Action">> => <<"Eval">>, + data => Code, + target => ProcID + }, Wallet), + <<"Assignment">> => + hb_message:sign(#{ <<"Block-Height">> => 1 }, Wallet) + }. + +basic_aos_call_test() -> + Msg = generate_stack("test/aos-2-pure-xs.wasm"), + Proc = hb_converge:get(<<"Process">>, Msg, #{ hashpath => ignore }), + ProcID = hb_converge:get(id, Proc, #{}), + {ok, Msg3} = + hb_converge:resolve( + Msg, + generate_aos_msg(ProcID, <<"return 1+1">>), + #{} + ), + Data = hb_converge:get(<<"Results/Data">>, Msg3, #{}), + ?assertEqual(<<"2">>, Data). \ No newline at end of file diff --git a/src/dev_message.erl b/src/dev_message.erl index 39638d71..0d855e38 100644 --- a/src/dev_message.erl +++ b/src/dev_message.erl @@ -2,15 +2,22 @@ %%% in the message's underlying Erlang map. Private keys (`priv[.*]') are %%% not included. -module(dev_message). --export([info/0, keys/1, id/1, unsigned_id/1, signers/1]). +-export([info/0, keys/1, id/1, unsigned_id/1, signed_id/1, signers/1]). -export([set/3, remove/2, get/2, get/3]). -include_lib("eunit/include/eunit.hrl"). -include("include/hb.hrl"). - - %% The list of keys that are exported by this device. --define(DEVICE_KEYS, [path, id, unsigned_id, signers, keys, get, set, remove]). +-define(DEVICE_KEYS, [ + path, + id, + unsigned_id, + signed_id, + signers, + keys, get, + set, + remove +]). %% @doc Return the info for the identity device. info() -> @@ -23,11 +30,10 @@ info() -> %% that. Otherwise, return the signed ID. id(M) -> ID = - case maps:get(signature, M, ?DEFAULT_SIG) of - ?DEFAULT_SIG -> raw_id(M, unsigned); + case get(signature, M) of + {error, not_found} -> raw_id(M, unsigned); _ -> raw_id(M, signed) end, - ?event({generated_id, {id, ID}, {msg, M}}), {ok, ID}. %% @doc Wrap a call to the `hb_util:id/2' function, which returns the @@ -35,6 +41,14 @@ id(M) -> unsigned_id(M) -> {ok, raw_id(M, unsigned)}. +%% @doc Return the signed ID of a message. +signed_id(M) -> + try + {ok, raw_id(M, signed)} + catch + _:_ -> {error, not_signed} + end. + %% @doc Encode an ID in any format to a normalized, b64u 43 character binary. raw_id(Item) -> raw_id(Item, unsigned). raw_id(TX, Type) when is_record(TX, tx) -> diff --git a/src/dev_multipass.erl b/src/dev_multipass.erl index f58736a6..cc5b2c49 100644 --- a/src/dev_multipass.erl +++ b/src/dev_multipass.erl @@ -1,17 +1,32 @@ +%%% @doc A device that triggers repass events until a certain counter has been +%%% reached. This is useful for certain types of stacks that need various +%%% execution passes to be completed in sequence across devices. -module(dev_multipass). --export([init/2, execute/2, uses/0]). +-export([init/3, compute/3]). +-include_lib("eunit/include/eunit.hrl"). -%%% A device that triggers repass events until a certain counter has been reached. -%%% This is useful for certain types of stacks that need various execution passes -%%% to be completed in sequence across devices. +init(M1, _M2, _Opts) -> + {ok, M1}. -init(S, Params) -> - {<<"Passes">>, Passes} = lists:keyfind(<<"Passes">>, 1, Params), - {ok, S#{ pass => 1, passes => binary_to_integer(Passes) }}. +compute(Msg1, _Msg2, Opts) -> + Passes = hb_converge:get(<<"Passes">>, Msg1, 1, Opts), + Pass = hb_converge:get(<<"Pass">>, Msg1, 1, Opts), + case Pass < Passes of + true -> + {pass, Msg1}; + false -> + {ok, Msg1} + end. -execute(_, S = #{ pass := Pass, passes := Passes }) when Pass < Passes -> - {pass, S}; -execute(_, S) -> - {ok, S}. +%%% Tests -uses() -> all. \ No newline at end of file +basic_multipass_test() -> + Msg1 = + #{ + <<"device">> => <<"Multipass/1.0">>, + <<"Passes">> => 2, + <<"Pass">> => 1 + }, + Msg2 = Msg1#{ <<"Pass">> => 2 }, + ?assertMatch({pass, _}, hb_converge:resolve(Msg1, <<"Compute">>, #{})), + ?assertMatch({ok, _}, hb_converge:resolve(Msg2, <<"Compute">>, #{})). diff --git a/src/dev_process.erl b/src/dev_process.erl index 83622a68..1dc985aa 100644 --- a/src/dev_process.erl +++ b/src/dev_process.erl @@ -45,7 +45,7 @@ %%% Public API -export([info/2, compute/3, schedule/3, slot/3, now/3]). %%% Test helpers --export([test_aos_process/0, test_device_process/0, test_wasm_process/1]). +-export([test_aos_process/0, dev_test_process/0, test_wasm_process/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("include/hb.hrl"). @@ -292,30 +292,56 @@ test_wasm_process(WASMImage) -> %% @doc Generate a process message with a random number, and the %% `dev_wasm' device for execution. test_aos_process() -> - test_wasm_process(<<"test/aos-2-pure.wasm">>). + Wallet = hb:wallet(), + WASMProc = test_wasm_process(<<"test/aos-2-pure-xs.wasm">>), + hb_message:sign(maps:merge(WASMProc, #{ + <<"Device-Stack">> => + [ + <<"WASI/1.0">>, + <<"JSON-Iface/1.0">>, + <<"WASM-64/1.0">>, + <<"Multipass/1.0">> + ], + <<"Passes">> => 2, + <<"Stack-Keys">> => [<<"Init">>, <<"Compute">>], + <<"Scheduler">> => hb:address(), + <<"Authority">> => hb:address() + }), Wallet). %% @doc Generate a device that has a stack of two `dev_test's for %% execution. This should generate a message state has doubled %% `Already-Seen' elements for each assigned slot. -test_device_process() -> +dev_test_process() -> maps:merge(test_base_process(), #{ <<"Execution-Device">> => <<"Stack/1.0">>, <<"Device-Stack">> => [<<"Test-Device/1.0">>, <<"Test-Device/1.0">>] }). schedule_test_message(Msg1, Text) -> - Msg2 = #{ + schedule_test_message(Msg1, Text, #{}). +schedule_test_message(Msg1, Text, MsgBase) -> + Wallet = hb:wallet(), + Msg2 = hb_message:sign(#{ path => <<"Schedule">>, <<"Method">> => <<"POST">>, <<"Message">> => - #{ + MsgBase#{ <<"Type">> => <<"Message">>, <<"Test-Key">> => Text } - }, + }, Wallet), ?assertMatch({ok, _}, hb_converge:resolve(Msg1, Msg2, #{})), ok. +schedule_aos_call(Msg1, Code) -> + ProcID = hb_converge:get(id, Msg1, #{}), + Msg2 = #{ + <<"Action">> => <<"Eval">>, + data => Code, + target => ProcID + }, + schedule_test_message(Msg1, <<"TEST CALL">>, Msg2). + schedule_wasm_call(Msg1, FuncName, Params) -> Msg2 = #{ path => <<"Schedule">>, @@ -351,7 +377,7 @@ schedule_on_process_test() -> get_scheduler_slot_test() -> init(), - Msg1 = test_aos_process(), + Msg1 = test_base_process(), schedule_test_message(Msg1, <<"TEST TEXT 1">>), schedule_test_message(Msg1, <<"TEST TEXT 2">>), Msg2 = #{ @@ -365,7 +391,7 @@ get_scheduler_slot_test() -> recursive_resolve_test() -> init(), - Msg1 = test_aos_process(), + Msg1 = test_base_process(), schedule_test_message(Msg1, <<"TEST TEXT 1">>), CurrentSlot = hb_converge:resolve( @@ -382,7 +408,7 @@ recursive_resolve_test() -> test_device_compute_test() -> init(), - Msg1 = test_device_process(), + Msg1 = dev_test_process(), schedule_test_message(Msg1, <<"TEST TEXT 1">>), schedule_test_message(Msg1, <<"TEST TEXT 2">>), ?assertMatch( @@ -420,3 +446,19 @@ wasm_compute_test() -> ), ?event({computed_message, {msg4, Msg4}}), ?assertEqual([720.0], hb_converge:get(<<"Results/WASM/Output">>, Msg4, #{})). + +aos_compute_test() -> + init(), + Msg1 = test_aos_process(), + schedule_aos_call(Msg1, <<"return 1+1">>), + schedule_aos_call(Msg1, <<"return 2+2">>), + Msg2 = #{ path => <<"Compute">>, <<"Slot">> => 0 }, + {ok, Msg3} = hb_converge:resolve(Msg1, Msg2, #{}), + {ok, Res} = hb_converge:resolve(Msg3, <<"Results">>, #{}), + ?event(debug, {computed_message, {msg3, Res}}), + {ok, Data} = hb_converge:resolve(Res, <<"Data">>, #{}), + ?event(debug, {computed_data, Data}), + ?assertEqual(<<"2">>, Data), + Msg4 = #{ path => <<"Compute">>, <<"Slot">> => 1 }, + {ok, Msg5} = hb_converge:resolve(Msg1, Msg4, #{}), + ?assertEqual(<<"4">>, hb_converge:get(<<"Results/Data">>, Msg5, #{})). \ No newline at end of file diff --git a/src/dev_stack.erl b/src/dev_stack.erl index 63a3663b..f09e92fe 100644 --- a/src/dev_stack.erl +++ b/src/dev_stack.erl @@ -181,7 +181,7 @@ transform(Msg1, Key, Opts) -> Opts ); _ -> - ?event({no_device_key, Key}), + ?event({no_device_key, Key, {stack, StackMsg}}), not_found end end. @@ -191,7 +191,7 @@ transform(Msg1, Key, Opts) -> resolve_stack(Message1, Key, Message2, Opts) -> resolve_stack(Message1, Key, Message2, 1, Opts). resolve_stack(Message1, Key, Message2, DevNum, Opts) -> - case transform(Message1, integer_to_binary(DevNum), Opts) of + case transform(Message1, DevNum, Opts) of {ok, Message3} -> ?event({stack_execute, DevNum, {msg1, Message3}, {msg2, Message2}}), case hb_converge:resolve(Message3, Message2, Opts) of @@ -205,47 +205,27 @@ resolve_stack(Message1, Key, Message2, DevNum, Opts) -> ?event({result, skip, DevNum, Message4}), {ok, Message4}; {pass, Message4} when is_map(Message4) -> - case hb_opts:get(allow_multipass, true, Opts) of - true -> - ?event({result, pass, allowed, DevNum, Message4}), - resolve_stack( - hb_converge:set( - Message4, - #{ - pass => - hb_converge:get( - pass, - Message4, - Opts - ) + 1 - }, - Opts - ), - Key, - Message2, - 1, - Opts - ); - _ -> - ?event( - {result, - pass, - not_allowed, - DevNum, - Message4 - } - ), - maybe_error( - Message1, - Key, - Message2, - DevNum + 1, - Opts, - {pass_not_allowed, Message4} - ) - end; + ?event({result, pass, {dev, DevNum}, Message4}), + resolve_stack( + hb_converge:set( + Message4, + #{ + pass => + hb_converge:get( + pass, + Message4, + Opts + ) + 1 + }, + Opts + ), + Key, + Message2, + 1, + Opts + ); {error, Info} -> - ?event({result, error, DevNum, Info}), + ?event({result, error, {dev, DevNum}, Info}), maybe_error( Message1, Key, @@ -255,7 +235,7 @@ resolve_stack(Message1, Key, Message2, DevNum, Opts) -> Info ); Unexpected -> - ?event({result, unexpected, DevNum, Unexpected}), + ?event({result, unexpected, {dev, DevNum}, Unexpected}), maybe_error( Message1, Key, diff --git a/src/dev_wasi.erl b/src/dev_wasi.erl index 73080a99..574d6582 100644 --- a/src/dev_wasi.erl +++ b/src/dev_wasi.erl @@ -42,6 +42,7 @@ %% - WASI-preview-1 compatible functions for accessing the filesystem %% - File descriptors for those files. init(M1, _M2, Opts) -> + ?event(running_init), MsgWithLib = hb_converge:set( M1, @@ -276,10 +277,11 @@ wasi_stack_is_serializable_test() -> Msg2 = hb_message:tx_to_message(hb_message:message_to_tx(Msg)), ?assert(hb_message:match(Msg, Msg2)). + basic_aos_exec_test() -> Init = generate_wasi_stack("test/aos-2-pure-xs.wasm", <<"handle">>, []), - Msg = dev_wasm:gen_test_aos_msg("return 1+1"), - Env = dev_wasm:gen_test_env(), + Msg = gen_test_aos_msg("return 1+1"), + Env = gen_test_env(), Port = hb_private:get(<<"WASM/Port">>, Init, #{}), {ok, Ptr1} = hb_beamr_io:malloc(Port, byte_size(Msg)), ?assertNotEqual(0, Ptr1), @@ -300,3 +302,10 @@ basic_aos_exec_test() -> #{ <<"response">> := #{ <<"Output">> := #{ <<"data">> := Data }} } = jiffy:decode(Output, [return_maps]), ?assertEqual(<<"2">>, Data). + +%%% Test Helpers +gen_test_env() -> + <<"{\"Process\":{\"Id\":\"AOS\",\"Owner\":\"FOOBAR\",\"Tags\":[{\"name\":\"Name\",\"value\":\"Thomas\"}, {\"name\":\"Authority\",\"value\":\"FOOBAR\"}]}}\0">>. + +gen_test_aos_msg(Command) -> + <<"{\"From\":\"FOOBAR\",\"Block-Height\":\"1\",\"Target\":\"AOS\",\"Owner\":\"FOOBAR\",\"Id\":\"1\",\"Module\":\"W\",\"Tags\":[{\"name\":\"Action\",\"value\":\"Eval\"}],\"Data\":\"", (list_to_binary(Command))/binary, "\"}\0">>. \ No newline at end of file diff --git a/src/dev_wasm.erl b/src/dev_wasm.erl index 9954a904..89e1cc17 100644 --- a/src/dev_wasm.erl +++ b/src/dev_wasm.erl @@ -7,10 +7,9 @@ %%% M1/Init -> %%% Assumes: %%% M1/Process -%%% M1/Process/Image +%%% M1/Process/WASM-Image %%% Generates: %%% /priv/WASM/Port -%%% /priv/WASM/Handler %%% /priv/WASM/Import-Resolver %%% Side-effects: %%% Creates a WASM executor loaded in memory of the HyperBEAM node. @@ -21,22 +20,26 @@ %%% M1/priv/WASM/Import-Resolver %%% M1/Process %%% M2/Message +%%% M2/Message/WASM-Function OR M1/WASM-Function +%%% M2/Message/WASM-Params OR M1/WASM-Params %%% Generates: %%% /Results/WASM/Type %%% /Results/WASM/Body %%% Side-effects: -%%% Calls the WASM executor with the message and process.''' +%%% Calls the WASM executor with the message and process. +%%% ''' -module(dev_wasm). -export([init/3, compute/3, import/3, terminate/3]). -export([wasm_state/3]). %%% Test API: --export([store_wasm_image/1, gen_test_env/0, gen_test_aos_msg/1]). +-export([store_wasm_image/1]). -include("include/hb.hrl"). -include_lib("eunit/include/eunit.hrl"). %% @doc Boot a WASM image on the image stated in the `Process/Image' field of %% the message. init(M1, _M2, Opts) -> + ?event(running_init), ImageID = case hb_converge:get(<<"WASM-Image">>, M1, Opts) of not_found -> @@ -63,6 +66,7 @@ init(M1, _M2, Opts) -> ?event(wasm_deserialized) end, % Set the WASM port, handler, and standard library invokation function. + ?event({setting_wasm_port, Port}), {ok, hb_private:set(M1, #{ @@ -102,6 +106,7 @@ default_import_resolver(Msg1, Msg2, Opts) -> %% @doc Call the WASM executor with a message that has been prepared by a prior %% pass. compute(M1, M2, Opts) -> + ?event(running_compute), case hb_converge:get(pass, M1, Opts) of X when X == 1 orelse X == not_found -> % Extract the WASM port, func, params, and standard library @@ -273,12 +278,4 @@ imported_function_test() -> #{ device => <<"Test-Device/1.0">> } } ) - ). - -%%% External AOS Test Helpers - -gen_test_env() -> - <<"{\"Process\":{\"Id\":\"AOS\",\"Owner\":\"FOOBAR\",\"Tags\":[{\"name\":\"Name\",\"value\":\"Thomas\"}, {\"name\":\"Authority\",\"value\":\"FOOBAR\"}]}}\0">>. - -gen_test_aos_msg(Command) -> - <<"{\"From\":\"FOOBAR\",\"Block-Height\":\"1\",\"Target\":\"AOS\",\"Owner\":\"FOOBAR\",\"Id\":\"1\",\"Module\":\"W\",\"Tags\":[{\"name\":\"Action\",\"value\":\"Eval\"}],\"Data\":\"", (list_to_binary(Command))/binary, "\"}\0">>. + ). \ No newline at end of file diff --git a/src/hb_converge.erl b/src/hb_converge.erl index 8bdf0fbb..643ba2a1 100644 --- a/src/hb_converge.erl +++ b/src/hb_converge.erl @@ -135,15 +135,21 @@ resolve(Msg1, Msg2, Opts) -> %% 9: Recurse, fork, or terminate. resolve_stage(0, Msg1, Msg2, Opts) when is_list(Msg1) -> - ?event(converge_core, {stage, 0, list_normalize}, Opts), % Normalize lists to numbered maps (base=1) if necessary. - resolve_stage(0, + ?event(converge_core, {stage, 0, list_normalize}, Opts), + NormMsg1 = maps:from_list( lists:zip( - lists:seq(1, length(Msg1)), + [ + key_to_binary(Key) + || + Key <- lists:seq(1, length(Msg1)) + ], Msg1 ) ), + resolve_stage(0, + NormMsg1, Msg2, Opts ); @@ -1372,4 +1378,12 @@ denormalized_device_key_test() -> element(3, message_to_fun(Msg, test_func, #{})), module ) - ). \ No newline at end of file + ). + +list_transform_test() -> + Msg = [<<"A">>, <<"B">>, <<"C">>, <<"D">>, <<"E">>], + ?assertEqual(<<"A">>, hb_converge:get(1, Msg)), + ?assertEqual(<<"B">>, hb_converge:get(2, Msg)), + ?assertEqual(<<"C">>, hb_converge:get(3, Msg)), + ?assertEqual(<<"D">>, hb_converge:get(4, Msg)), + ?assertEqual(<<"E">>, hb_converge:get(5, Msg)). diff --git a/src/hb_message.erl b/src/hb_message.erl index 20f88a00..b252fcbd 100644 --- a/src/hb_message.erl +++ b/src/hb_message.erl @@ -47,18 +47,41 @@ format(Bin, Indent) when is_binary(Bin) -> Indent ); format(Map, Indent) when is_map(Map) -> - Header = hb_util:format_indented("Message {~n", Indent), + SignedID = hb_converge:get(signed_id, Map), + UnsignedID = hb_converge:get(unsigned_id, Map), + IDStr = + case {SignedID, UnsignedID} of + {not_found, not_found} -> ""; + {_, _} when (SignedID == UnsignedID) or (SignedID == not_found) -> + io_lib:format("[ U: ~s ] ", + [hb_util:short_id(UnsignedID)]); + {_, not_found} -> + io_lib:format("[ ID: ~s ] ", [hb_util:short_id(SignedID)]); + {_, _} -> + io_lib:format("[ ID: ~s, U: ~s ] ", + [hb_util:short_id(SignedID), hb_util:short_id(UnsignedID)]) + end, + SignerStr = + case signers(Map) of + [] -> ""; + [Signer] -> + io_lib:format(" [Signer: ~s] ", [hb_util:short_id(Signer)]); + Signers -> + io_lib:format(" [Signers: ~s] ", + [string:join(lists:map(fun hb_util:short_id/1, Signers), ", ")]) + end, + Header = + hb_util:format_indented("Message ~s~s{~n", + [lists:flatten(IDStr), SignerStr], Indent), Res = lists:map( fun({Key, Val}) -> NormKey = hb_converge:to_key(Key, #{ error_strategy => ignore }), KeyStr = case NormKey of - Key -> - io_lib:format("~p", [NormKey]); undefined -> io_lib:format("~p [!!! INVALID KEY !!!]", [Key]); _ -> - io_lib:format("~p [raw: ~p]", [NormKey, Key]) + io_lib:format("~s", [hb_converge:key_to_binary(Key)]) end, hb_util:format_indented( "~s := ~s~n", @@ -67,6 +90,9 @@ format(Map, Indent) when is_map(Map) -> case Val of NextMap when is_map(NextMap) -> hb_util:format_map(NextMap, Indent + 2); + _ when (byte_size(Val) == 32) or (byte_size(Val) == 43) -> + Short = hb_util:short_id(Val), + io_lib:format("~s*", [Short]); Bin when is_binary(Bin) -> hb_util:format_binary(Bin); Other -> @@ -76,7 +102,7 @@ format(Map, Indent) when is_map(Map) -> Indent + 1 ) end, - maps:to_list(Map) + maps:to_list(minimize(Map)) ), case Res of [] -> "[Empty map]"; @@ -92,9 +118,10 @@ format(Item, Indent) -> %% @doc Return the signers of a message. For now, this is just the signer %% of the message itself. In the future, we will support multiple signers. signers(Msg) when is_map(Msg) -> - case ar_bundles:signer(message_to_tx(Msg)) of - undefined -> []; - Signer -> [Signer] + case {maps:find(owner, Msg), maps:find(signature, Msg)} of + {_, error} -> []; + {error, _} -> []; + {{ok, Owner}, {ok, _}} -> [ar_wallet:to_address(Owner)] end; signers(TX) when is_record(TX, tx) -> ar_bundles:signer(TX); @@ -180,13 +207,15 @@ normalize_keys(Map) -> ) ). -%% @doc Remove keys from the map that can be regenerated. -minimize(RawVal) when not is_map(RawVal) -> RawVal; -minimize(Map) -> - NormRegenKeys = normalize_keys(?REGEN_KEYS), +%% @doc Remove keys from the map that can be regenerated. Optionally takes an +%% additional list of keys to include in the minimization. +minimize(Msg) -> minimize(Msg, []). +minimize(RawVal, _) when not is_map(RawVal) -> RawVal; +minimize(Map, ExtraKeys) -> + NormKeys = normalize_keys(?REGEN_KEYS) ++ normalize_keys(ExtraKeys), maps:filter( fun(Key, _) -> - (not lists:member(hb_converge:key_to_binary(Key), NormRegenKeys)) + (not lists:member(hb_converge:key_to_binary(Key), NormKeys)) andalso (not hb_private:is_private(Key)) end, maps:map(fun(_K, V) -> minimize(V) end, Map) @@ -316,17 +345,27 @@ message_to_tx(RawM) when is_map(RawM) -> NormalizedMsgKeyMap = normalize_keys(MsgKeyMap), % Iterate through the default fields, replacing them with the values from % the message map if they are present. - {RemainingMap, BaseTXList} = lists:foldl( - fun({Field, Default}, {RemMap, Acc}) -> - NormKey = hb_converge:key_to_binary(Field), - case maps:find(NormKey, NormalizedMsgKeyMap) of - error -> {RemMap, [Default | Acc]}; - {ok, Value} -> {maps:remove(NormKey, RemMap), [Value | Acc]} - end - end, - {NormalizedMsgKeyMap, []}, - default_tx_list() - ), + {RemainingMap, BaseTXList} = + lists:foldl( + fun({Field, Default}, {RemMap, Acc}) -> + NormKey = hb_converge:key_to_binary(Field), + case maps:find(NormKey, NormalizedMsgKeyMap) of + error -> {RemMap, [Default | Acc]}; + {ok, Value} when is_binary(Default) andalso ?IS_ID(Value) -> + { + maps:remove(NormKey, RemMap), + [hb_util:native_id(Value)|Acc] + }; + {ok, Value} -> + { + maps:remove(NormKey, RemMap), + [Value|Acc] + } + end + end, + {NormalizedMsgKeyMap, []}, + default_tx_list() + ), % Rebuild the tx record from the new list of fields and values. TXWithoutTags = list_to_tuple([tx | lists:reverse(BaseTXList)]), % Calculate which set of the remaining keys will be used as tags. @@ -606,6 +645,20 @@ single_layer_message_to_tx_test() -> ?assertEqual({<<"Special-Key">>, <<"SPECIAL_VALUE">>}, lists:keyfind(<<"Special-Key">>, 1, TX#tx.tags)). +% %% @doc Test that different key encodings are converted to their corresponding +% %% TX fields. +% key_encodings_to_tx_test() -> +% Msg = #{ +% <<"last_tx">> => << 2:256 >>, +% <<"Owner">> => << 3:4096 >>, +% <<"Target">> => << 4:256 >> +% }, +% TX = message_to_tx(Msg), +% ?event({key_encodings_to_tx, {msg, Msg}, {tx, TX}}), +% ?assertEqual(maps:get(<<"last_tx">>, Msg), TX#tx.last_tx), +% ?assertEqual(maps:get(<<"Owner">>, Msg), TX#tx.owner), +% ?assertEqual(maps:get(<<"Target">>, Msg), TX#tx.target). + %% @doc Test that we can convert a #tx record into a message correctly. single_layer_tx_to_message_test() -> TX = #tx { @@ -728,7 +781,6 @@ deeply_nested_message_with_data_test() -> }, ?assert(match(Msg, tx_to_message(message_to_tx(Msg)))). - nested_structured_fields_test() -> NestedMsg = #{ a => #{ b => 1 } }, ?assert( @@ -858,4 +910,4 @@ empty_string_in_tag_test() -> } }, Msg2 = minimize(tx_to_message(message_to_tx(Msg))), - ?assert(match(Msg, Msg2)). + ?assert(match(Msg, Msg2)). \ No newline at end of file diff --git a/src/hb_opts.erl b/src/hb_opts.erl index 527b0864..ff636009 100644 --- a/src/hb_opts.erl +++ b/src/hb_opts.erl @@ -51,16 +51,16 @@ config() -> <<"Test-Device/1.0">> => dev_test, <<"Message/1.0">> => dev_message, <<"Stack/1.0">> => dev_stack, + <<"Multipass/1.0">> => dev_multipass, <<"Scheduler/1.0">> => dev_scheduler, <<"Process/1.0">> => dev_process, <<"WASM-64/1.0">> => dev_wasm, <<"WASI/1.0">> => dev_wasi, + <<"JSON-Iface/1.0">> => dev_json_iface, <<"Cron">> => dev_cron, <<"Deduplicate">> => dev_dedup, - <<"JSON-Interface">> => dev_json_iface, <<"PODA">> => dev_poda, <<"Monitor">> => dev_monitor, - <<"Multipass">> => dev_multipass, <<"Push">> => dev_mu, <<"Compute">> => dev_cu, <<"P4">> => dev_p4 diff --git a/src/hb_util.erl b/src/hb_util.erl index 13eeca59..f548aa5d 100644 --- a/src/hb_util.erl +++ b/src/hb_util.erl @@ -1,6 +1,6 @@ %% @doc A collection of utility functions for building with HyperBEAM. -module(hb_util). --export([id/1, id/2, native_id/1, human_id/1]). +-export([id/1, id/2, native_id/1, human_id/1, short_id/1]). -export([encode/1, decode/1, safe_encode/1, safe_decode/1]). -export([find_value/2, find_value/3]). -export([number/1, list_to_numbered_map/1, message_to_numbered_list/1]). @@ -62,6 +62,12 @@ human_id(Bin) when is_binary(Bin) andalso byte_size(Bin) == 32 -> human_id(Bin) when is_binary(Bin) andalso byte_size(Bin) == 43 -> Bin. +short_id(Bin) when is_binary(Bin) andalso byte_size(Bin) == 32 -> + short_id(human_id(Bin)); +short_id(Bin) when is_binary(Bin) andalso byte_size(Bin) == 43 -> + << FirstTag:5/binary, _:33/binary, LastTag:5/binary >> = Bin, + << FirstTag/binary, "..", LastTag/binary >>. + %% @doc Encode a binary to URL safe base64 binary string. encode(Bin) -> b64fast:encode(Bin).