From c9345108595f3694e4321dd778c818c4aad2b7e0 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Tue, 30 Sep 2025 16:14:11 +1000 Subject: [PATCH 1/3] Complete AACI definition This commit combines 13 separate commits: add more atoms to AACI serialize signatures This took a surprising number of goose chases to work out... I had to find out - what is the gmser prefix for a signature (sg_) - what is the gmb wrapper for a signature (none) - what errors gmser can report when a signature is invalid - what an example of a valid signature is - what that example signature serializes to coerce stringy booleans coerce bytes coerce bits The thing to remember about bits is that they are actually integers... It is tempting to present bits as binaries, but that hides the nuance of the infinite leading zeroes, the potential for infinite leading ones, etc. coerce character It's really just an integer... Should we flatten it to an integer instead? I don't know. Also coerce unicode strings to FATE This is mainly so that gajudesk can pass text box content to hz as-is, but also allows users to pass utf8 binaries in, if they want to, for some reason. Coerce binaries as-is Sophia accepts both sg_... and #... as signatures, so we should probably accept binaries as signatures directly. People might expect to be able to put the listy string "#..." in too, but that is more complex to do. coerce hashes It turns out there are a lot of types that, like option, should only be valid as an opaque/normalized type, but should be substituted for something different in the flat representation. If we restructure things a little then we can implement all of these in one go. Refactor type normalization Some of these checks were redundant, and we probably don't actually need substitution to wrap success/failure, since it isn't expected to fail anyway... Now the logic is much simpler, and adding more built-in type definitions should be easy. Add a map for builtin types This makes it much easier to implement all these standard library things. In doing so I changed the convention for option, hash, unit, to be stringy rather than atoms. Also I changed some error messages based on what was more helpful during debugging of the unit tests. Add more builtin types We probably should extract these from the standard library instead of cherry picking the ones that are needed by the chain? e.g. Chain.tx still doesn't work. remaining types `tx` isn't defined in all the same places that pointee, name, base_tx, fr, fp are defined, but actually it is the only one not in the list I was looking at, so we are all good. As demonstration, there is also a test case for Set.set, despite Set.set not being defined as a builtin type. --- src/hz.erl | 491 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 374 insertions(+), 117 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index ffe0168..d255c24 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -1419,7 +1419,8 @@ prepare_aaci(ACI) -> % down to the concrete types they actually represent. We annotate each % subexpression of this concrete type with other info too, in case it helps % make error messages easier to understand. - Specs = annotate_function_specs(OpaqueSpecs, TypeDefs, #{}), + InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs), + Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}), {aaci, Name, Specs, TypeDefs}. @@ -1520,20 +1521,85 @@ opaque_type(Params, #{variant := VariantDefs}) -> {variant, Variants}; opaque_type(Params, #{tuple := TypeDefs}) -> {tuple, [opaque_type(Params, Type) || Type <- TypeDefs]}; +opaque_type(_, #{bytes := Count}) -> + {bytes, [Count]}; opaque_type(Params, Pair) when is_map(Pair) -> [{Name, TypeArgs}] = maps:to_list(Pair), {opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}. -% atoms for builtins, strings (lists) for user-defined types -opaque_type_name(<<"int">>) -> integer; -opaque_type_name(<<"address">>) -> address; -opaque_type_name(<<"contract">>) -> contract; -opaque_type_name(<<"bool">>) -> boolean; -opaque_type_name(<<"option">>) -> option; -opaque_type_name(<<"list">>) -> list; -opaque_type_name(<<"map">>) -> map; -opaque_type_name(<<"string">>) -> string; -opaque_type_name(Name) -> binary_to_list(Name). +% Atoms for any builtins that aren't qualified by a namespace in Sophia. +% Everything else stays as a string, user-defined or not. +opaque_type_name(<<"int">>) -> integer; +opaque_type_name(<<"bool">>) -> boolean; +opaque_type_name(<<"bits">>) -> bits; +opaque_type_name(<<"char">>) -> char; +opaque_type_name(<<"string">>) -> string; +opaque_type_name(<<"address">>) -> address; +opaque_type_name(<<"signature">>) -> signature; +opaque_type_name(<<"contract">>) -> contract; +opaque_type_name(<<"list">>) -> list; +opaque_type_name(<<"map">>) -> map; +% I'm not sure how to produce channels in Sophia source, but they seem to exist +% in gmb still. +opaque_type_name(<<"channel">>) -> channel; +opaque_type_name(Name) -> binary_to_list(Name). + +builtin_typedefs() -> + #{"unit" => {[], {tuple, []}}, + "void" => {[], {variant, []}}, + "hash" => {[], {bytes, [32]}}, + "option" => {["'T"], {variant, [{"None", []}, + {"Some", [{var, "'T"}]}]}}, + "Chain.ttl" => {[], {variant, [{"FixedTTL", [integer]}, + {"RelativeTTL", [integer]}]}}, + "AENS.pointee" => {[], {variant, [{"AccountPt", [address]}, + {"OraclePt", [address]}, + {"ContractPt", [address]}, + {"ChannelPt", [address]}]}}, + "AENS.name" => {[], {variant, [{"Name", [address, + "Chain.ttl", + {map, [string, "AENS.pointee"]}]}]}}, + "AENSv2.pointee" => {[], {variant, [{"AccountPt", [address]}, + {"OraclePt", [address]}, + {"ContractPt", [address]}, + {"ChannelPt", [address]}, + {"DataPt", [{bytes, [any]}]}]}}, + "AENSv2.name" => {[], {variant, [{"Name", [address, + "Chain.ttl", + {map, [string, "AENSv2.pointee"]}]}]}}, + "Chain.ga_meta_tx" => {[], {variant, [{"GAMetaTx", [address, integer]}]}}, + "Chain.paying_for_tx" => {[], {variant, [{"PayingForTx", [address, integer]}]}}, + "Chain.base_tx" => {[], {variant, [{"SpendTx", [address, integer, string]}, + {"OracleRegisterTx", []}, + {"OracleQueryTx", []}, + {"OracleResponseTx", []}, + {"OracleExtendTx", []}, + {"NamePreclaimTx", []}, + {"NameClaimTx", ["hash"]}, + {"NameUpdateTx", [string]}, + {"NameRevokeTx", ["hash"]}, + {"NameTransferTx", [address, string]}, + {"ChannelCreateTx", [address]}, + {"ChannelDepositTx", [address, integer]}, + {"ChannelWithdrawTx", [address, integer]}, + {"ChannelForceProgressTx", [address]}, + {"ChannelCloseMutualTx", [address]}, + {"ChannelCloseSoloTx", [address]}, + {"ChannelSlashTx", [address]}, + {"ChannelSettleTx", [address]}, + {"ChannelSnapshotSoloTx", [address]}, + {"ContractCreateTx", [integer]}, + {"ContractCallTx", [address, integer]}, + {"GAAttachTx", []}]}}, + "Chain.tx" => {[], {record, [{"paying_for", {"option", ["Chain.paying_for_tx"]}}, + {"ga_metas", {list, ["Chain.ga_meta_tx"]}}, + {"actor", address}, + {"fee", integer}, + {"ttl", integer}, + {"tx", "Chain.base_tx"}]}}, + "MCL_BLS12_381.fr" => {[], {bytes, [32]}}, + "MCL_BLS12_381.fp" => {[], {bytes, [48]}} + }. % Type preparation has two goals. First, we need a data structure that can be % traversed quickly, to take sophia-esque erlang expressions and turn them into @@ -1576,6 +1642,10 @@ annotate_type(T, Types) -> Error end. +annotate_type2(T, _, _, unknown_type, _) -> + % If a type is unknown, then it should not be reported as the normalized + % name. + {ok, {T, unknown_type, unknown_type}}; annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) -> case annotate_type_subexpressions(NExpanded, Types) of {ok, Flat} -> @@ -1597,6 +1667,10 @@ annotate_types([], _Types, Acc) -> annotate_type_subexpressions(PrimitiveType, _Types) when is_atom(PrimitiveType) -> {ok, PrimitiveType}; +annotate_type_subexpressions({bytes, [Count]}, _Types) -> + % bytes is weird, because it has an argument, but that argument isn't an + % opaque type. + {ok, {bytes, [Count]}}; annotate_type_subexpressions({variant, VariantsOpaque}, Types) -> case annotate_variants(VariantsOpaque, Types, []) of {ok, Variants} -> {ok, {variant, Variants}}; @@ -1629,116 +1703,99 @@ annotate_variants([{Name, Elems} | Rest], Types, Acc) -> annotate_variants([], _Types, Acc) -> {ok, lists:reverse(Acc)}. -normalize_opaque_type(T, Types) -> - case type_is_expanded(T) of - false -> normalize_opaque_type(T, Types, true); - true -> {ok, true, T, T} - end. +% This function evaluates type aliases in a loop, until eventually a usable +% definition is found. +normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types, true). % FIXME detect infinite loops % FIXME detect builtins with the wrong number of arguments % FIXME should nullary types have an empty list of arguments added before now? -normalize_opaque_type({option, [T]}, _Types, IsFirst) -> - % Just like user-made ADTs, 'option' is considered part of the type, and so - % options are considered normalised. - {ok, IsFirst, {option, [T]}, {variant, [{"None", []}, {"Some", [T]}]}}; +normalize_opaque_type(T, _Types, IsFirst) when is_atom(T) -> + % Once we have eliminated the above rewrite cases, all other cases are + % handled explicitly by the coerce logic, and so are considered normalized. + {ok, IsFirst, T, T}; +normalize_opaque_type(Type = {T, _}, _Types, IsFirst) when is_atom(T) -> + % Once we have eliminated the above rewrite cases, all other cases are + % handled explicitly by the coerce logic, and so are considered normalized. + {ok, IsFirst, Type, Type}; normalize_opaque_type(T, Types, IsFirst) when is_list(T) -> + % Lists/strings indicate userspace types, which may require arg + % substitutions. Convert to an explicit but empty arg list, for uniformity. normalize_opaque_type({T, []}, Types, IsFirst); normalize_opaque_type({T, TypeArgs}, Types, IsFirst) when is_list(T) -> case maps:find(T, Types) of - %{error, invalid_aci}; % FIXME more info error -> - {ok, IsFirst, {T, TypeArgs}, {unknown_type, TypeArgs}}; + % We couldn't find this named type... Keep building the AACI, but + % mark this type expression as unknown, so that FATE coercions + % aren't attempted. + {ok, IsFirst, {T, TypeArgs}, unknown_type}; {ok, {TypeParamNames, Definition}} -> - Bindings = lists:zip(TypeParamNames, TypeArgs), - normalize_opaque_type2(T, TypeArgs, Types, IsFirst, Bindings, Definition) + % We have a definition for this type, including names for whatever + % args we have been given. Subtitute our args into this. + NewType = substitute_opaque_type(TypeParamNames, Definition, TypeArgs), + % Now continue on to see if we need to restart the loop or not. + normalize_opaque_type2(IsFirst, {T, TypeArgs}, NewType, Types) end. -normalize_opaque_type2(T, TypeArgs, Types, IsFirst, Bindings, Definition) -> - SubResult = - case Bindings of - [] -> {ok, Definition}; - _ -> substitute_opaque_type(Bindings, Definition) - end, - case SubResult of - % Type names were already normalized if they were ADTs or records, - % since for those connectives the name is considered part of the type. - {ok, NextT = {variant, _}} -> - {ok, IsFirst, {T, TypeArgs}, NextT}; - {ok, NextT = {record, _}} -> - {ok, IsFirst, {T, TypeArgs}, NextT}; - % Everything else has to be substituted down to a built-in connective - % to be considered normalized. - {ok, NextT} -> - normalize_opaque_type3(NextT, Types); - Error -> - Error - end. +normalize_opaque_type2(IsFirst, PrevType, NextType = {variant, _}, _) -> + % We have reduced to a variant. Report the type name as the normalized + % type, but also provide the variant definition itself as the candidate + % flattened type for further annotation. + {ok, IsFirst, PrevType, NextType}; +normalize_opaque_type2(IsFirst, PrevType, NextType = {record, _}, _) -> + % We have reduced to a record. Report the type name as the normalized + % type, but also provide the record definition itself as the candidate + % flattened type for further annotation. + {ok, IsFirst, PrevType, NextType}; +normalize_opaque_type2(_, _, NextType, Types) -> + % Not a variant or record yet, so go back to the start of the loop. + % It will no longer be the first iteration. + normalize_opaque_type(NextType, Types, false). -% while this does look like normalize_opaque_type/2, it sets IsFirst to false -% instead of true, and is part of the loop, instead of being an initial -% condition for the loop. -normalize_opaque_type3(NextT, Types) -> - case type_is_expanded(NextT) of - false -> normalize_opaque_type(NextT, Types, false); - true -> {ok, false, NextT, NextT} - end. +% Perform a beta-reduction on a type expression. +substitute_opaque_type([], Definition, _) -> + % There are no parameters to substitute. This is the simplest way of + % defining type aliases, records, and variants, so we should make sure to + % short circuit all the recursive descent logic, since it won't actually + % do anything. + Definition; +substitute_opaque_type(TypeParamNames, Definition, TypeArgs) -> + % Bundle the param names alongside the args that we want to substitute, so + % that we can keyfind the one list. + Bindings = lists:zip(TypeParamNames, TypeArgs), + substitute_opaque_type(Bindings, Definition). -% Strings indicate names that should be substituted. Atoms indicate built in -% types, which don't need to be expanded, except for option. -type_is_expanded({option, _}) -> false; -type_is_expanded(X) when is_atom(X) -> true; -type_is_expanded({X, _}) when is_atom(X) -> true; -type_is_expanded(_) -> false. - -% Skip traversal if there is nothing to substitute. This will often be the -% most common case. substitute_opaque_type(Bindings, {var, VarName}) -> case lists:keyfind(VarName, 1, Bindings) of - false -> {error, invalid_aci}; - {_, TypeArg} -> {ok, TypeArg} - end; -substitute_opaque_type(Bindings, {variant, Args}) -> - case substitute_variant_types(Bindings, Args, []) of - {ok, Result} -> {ok, {variant, Result}}; - Error -> Error - end; -substitute_opaque_type(Bindings, {record, Args}) -> - case substitute_record_types(Bindings, Args, []) of - {ok, Result} -> {ok, {record, Result}}; - Error -> Error + {_, TypeArg} -> TypeArg; + % No valid ACI will create this case. Regardless, the user should + % still be able to specify arbitrary gmb FATE terms for whatever this + % is meant to be. + false -> unknown_type end; +substitute_opaque_type(Bindings, {variant, Variants}) -> + Each = fun({VariantName, Elements}) -> + NewElements = substitute_opaque_types(Bindings, Elements), + {VariantName, NewElements} + end, + NewVariants = lists:map(Each, Variants), + {variant, NewVariants}; +substitute_opaque_type(Bindings, {record, Fields}) -> + Each = fun({FieldName, FieldType}) -> + NewType = substitute_opaque_type(Bindings, FieldType), + {FieldName, NewType} + end, + NewFields = lists:map(Each, Fields), + {record, NewFields}; substitute_opaque_type(Bindings, {Connective, Args}) -> - case substitute_opaque_types(Bindings, Args, []) of - {ok, Result} -> {ok, {Connective, Result}}; - Error -> Error - end; + NewArgs = substitute_opaque_types(Bindings, Args), + {Connective, NewArgs}; substitute_opaque_type(_Bindings, Type) -> - {ok, Type}. + Type. -substitute_variant_types(Bindings, [{VariantName, Elements} | Rest], Acc) -> - case substitute_opaque_types(Bindings, Elements, []) of - {ok, Result} -> substitute_variant_types(Bindings, Rest, [{VariantName, Result} | Acc]); - Error -> Error - end; -substitute_variant_types(_Bindings, [], Acc) -> - {ok, lists:reverse(Acc)}. - -substitute_record_types(Bindings, [{ElementName, Type} | Rest], Acc) -> - case substitute_opaque_type(Bindings, Type) of - {ok, Result} -> substitute_record_types(Bindings, Rest, [{ElementName, Result} | Acc]); - Error -> Error - end; -substitute_record_types(_Bindings, [], Acc) -> - {ok, lists:reverse(Acc)}. - -substitute_opaque_types(Bindings, [Next | Rest], Acc) -> - case substitute_opaque_type(Bindings, Next) of - {ok, Result} -> substitute_opaque_types(Bindings, Rest, [Result | Acc]); - Error -> Error - end; -substitute_opaque_types(_Bindings, [], Acc) -> - {ok, lists:reverse(Acc)}. +substitute_opaque_types(Bindings, Types) -> + Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end, + lists:map(Each, Types). coerce_bindings(VarTypes, Terms, Direction) -> DefLength = length(VarTypes), @@ -1788,33 +1845,37 @@ coerce({O, N, integer}, S, to_fate) when is_list(S) -> error:badarg -> single_error({invalid, O, N, S}) end; coerce({O, N, address}, S, to_fate) -> - try - case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of - {account_pubkey, Key} -> {ok, {address, Key}}; - _ -> single_error({invalid, O, N, S}) - end - catch - error:_ -> single_error({invalid, O, N, S}) - end; + coerce_chain_object(O, N, address, account_pubkey, S); coerce({_, _, address}, {address, Bin}, from_fate) -> Address = gmser_api_encoder:encode(account_pubkey, Bin), {ok, unicode:characters_to_list(Address)}; coerce({O, N, contract}, S, to_fate) -> - try - case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of - {contract_pubkey, Key} -> {ok, {contract, Key}}; - _ -> single_error({invalid, O, N, S}) - end - catch - error:_ -> single_error({invalid, O, N, S}) - end; + coerce_chain_object(O, N, contract, contract_pubkey, S); coerce({_, _, contract}, {contract, Bin}, from_fate) -> Address = gmser_api_encoder:encode(contract_pubkey, Bin), {ok, unicode:characters_to_list(Address)}; +coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) -> + % If it is a binary of 64 bytes then it can be used as is... If it is an + % sg_... string of 64 bytes, then it is too short to be valid, so just + % interpret it as a binary directly. + {ok, S}; +coerce({O, N, signature}, S, to_fate) -> + coerce_chain_object(O, N, signature, signature, S); +coerce({_, _, signature}, Bin, from_fate) -> + Address = gmser_api_encoder:encode(signature, Bin), + {ok, unicode:characters_to_list(Address)}; +%coerce({_, _, channel}, S, to_fate) when is_binary(S) -> + %{ok, {channel, S}}; +%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) -> + %{ok, S}; coerce({_, _, boolean}, true, _) -> {ok, true}; +coerce({_, _, boolean}, "true", _) -> + {ok, true}; coerce({_, _, boolean}, false, _) -> {ok, false}; +coerce({_, _, boolean}, "false", _) -> + {ok, false}; coerce({O, N, boolean}, S, _) -> single_error({invalid, O, N, S}); coerce({O, N, string}, Str, Direction) -> @@ -1830,6 +1891,30 @@ coerce({O, N, string}, Str, Direction) -> StrBin -> {ok, StrBin} end; +coerce({_, _, char}, Val, _Direction) when is_integer(Val) -> + {ok, Val}; +coerce({O, N, char}, Str, to_fate) -> + Result = unicode:characters_to_list(Str), + case Result of + {error, _, _} -> + single_error({invalid, O, N, Str}); + {incomplete, _, _} -> + single_error({invalid, O, N, Str}); + [C] -> + {ok, C}; + _ -> + single_error({invalid, O, N, Str}) + end; +coerce({O, N, {bytes, [Count]}}, Bytes, _Direction) when is_bitstring(Bytes) -> + coerce_bytes(O, N, Count, Bytes); +coerce({_, _, bits}, {bits, Num}, from_fate) -> + {ok, Num}; +coerce({_, _, bits}, Num, to_fate) when is_integer(Num) -> + {ok, {bits, Num}}; +coerce({_, _, bits}, Bits, to_fate) when is_bitstring(Bits) -> + Size = bit_size(Bits), + <> = Bits, + {ok, {bits, IntValue}}; coerce({_, _, {list, [Type]}}, Data, Direction) when is_list(Data) -> coerce_list(Type, Data, Direction); coerce({_, _, {map, [KeyType, ValType]}}, Data, Direction) when is_map(Data) -> @@ -1879,6 +1964,36 @@ coerce({O, N, _}, Data, from_fate) -> {ok, Data}; coerce({O, N, _}, Data, _) -> single_error({invalid, O, N, Data}). +coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> + single_error({partial_bytes, O, N, bit_size(Bytes)}); +coerce_bytes(_, _, any, Bytes) -> + {ok, Bytes}; +coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> + single_error({incorrect_size, O, N, Bytes}); +coerce_bytes(_, _, _, Bytes) -> + {ok, Bytes}. + +coerce_chain_object(O, N, T, Tag, S) -> + case decode_chain_object(Tag, S) of + {ok, Data} -> {ok, coerce_chain_object2(T, Data)}; + {error, Reason} -> single_error({Reason, O, N, S}) + end. + +coerce_chain_object2(address, Data) -> {address, Data}; +coerce_chain_object2(contract, Data) -> {contract, Data}; +coerce_chain_object2(signature, Data) -> Data. + +decode_chain_object(Tag, S) -> + try + case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of + {Tag, Data} -> {ok, Data}; + {_, _} -> {error, wrong_prefix} + end + catch + error:missing_prefix -> {error, missing_prefix}; + error:incorrect_size -> {error, incorrect_size} + end. + coerce_list(Type, Elements, Direction) -> % 0 index since it represents a sophia list coerce_list(Type, Elements, Direction, 0, [], []). @@ -2409,6 +2524,8 @@ try_coerce(Type, Sophia, Fate) -> _ -> erlang:error({from_fate_failed, Sophia, SophiaActual}) end, + % Finally, check that the FATE result is something that gmb understands. + gmb_fate_encoding:serialize(Fate), ok. coerce_int_test() -> @@ -2431,6 +2548,24 @@ coerce_contract_test() -> 167,208,53,78,40,235,2,163,132,36,47,183,228,151,9, 210,39,214>>}). +coerce_signature_test() -> + {ok, Type} = annotate_type(signature, #{}), + try_coerce(Type, + "sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n", + <<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135, + 169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222, + 249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114, + 233,110,119,143,206,137,124,36,63,154,85,7>>). + +coerce_signature_binary_test() -> + {ok, Type} = annotate_type(signature, #{}), + Binary = <<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135, + 169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222, + 249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114, + 233,110,119,143,206,137,124,36,63,154,85,7>>, + {ok, Binary} = coerce(Type, Binary, to_fate), + ok. + coerce_bool_test() -> {ok, Type} = annotate_type(boolean, #{}), try_coerce(Type, true, true), @@ -2459,10 +2594,40 @@ coerce_variant_test() -> try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}), try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}). +coerce_option_test() -> + {ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()), + try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}), + try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}). + coerce_record_test() -> {ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}), try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). +coerce_bytes_test() -> + {ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}), + try_coerce(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). + +coerce_bits_test() -> + {ok, Type} = annotate_type(bits, #{}), + try_coerce(Type, 5, {bits, 5}). + +coerce_char_test() -> + {ok, Type} = annotate_type(char, #{}), + try_coerce(Type, $?, $?). + +coerce_unicode_test() -> + {ok, Type} = annotate_type(char, #{}), + % Latin Small Letter C with cedilla and acute + {ok, $ḉ} = coerce(Type, <<"ḉ"/utf8>>, to_fate), + ok. + +coerce_hash_test() -> + {ok, Type} = annotate_type("hash", builtin_typedefs()), + Hash = list_to_binary(lists:seq(1,32)), + try_coerce(Type, Hash, Hash), + ok. + + %%% Complex AACI paramter and namespace tests @@ -2549,3 +2714,95 @@ param_test() -> try_coerce(Input, 0, 0), try_coerce(Output, 0, 0). +%%% Obscure Sophia types where we should check the AACI as well + +obscure_aaci_test() -> + Contract = " + include \"Set.aes\" + contract C = + entrypoint options(): option(int) = None + entrypoint fixed_bytes(): bytes(4) = #DEADBEEF + entrypoint any_bytes(): bytes() = Bytes.to_any_size(#112233) + entrypoint bits(): bits = Bits.all + entrypoint character(): char = 'a' + entrypoint hash(): hash = #00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF + entrypoint unit(): unit = () + + entrypoint ttl(x): Chain.ttl = FixedTTL(x) + entrypoint paying_for(x, y): Chain.paying_for_tx = Chain.PayingForTx(x, y) + entrypoint ga_meta_tx(x, y): Chain.ga_meta_tx = Chain.GAMetaTx(x, y) + entrypoint base_tx(x, y, z): Chain.base_tx = Chain.SpendTx(x, y, z) + entrypoint tx(a, b, c, d, e, f): Chain.tx = + {paying_for = a, + ga_metas = b, + actor = c, + fee = d, + ttl = e, + tx = f} + + entrypoint pointee(x): AENS.pointee = AENS.AccountPt(x) + entrypoint name(x, y, z): AENS.name = AENS.Name(x, y, z) + entrypoint pointee2(x): AENSv2.pointee = AENSv2.DataPt(x) + entrypoint name2(x, y, z): AENSv2.name = AENSv2.Name(x, y, z) + + entrypoint fr(x): MCL_BLS12_381.fr = x + entrypoint fp(x): MCL_BLS12_381.fp = x + + entrypoint set(): Set.set(int) = Set.new() + + ", + {ok, AACI} = aaci_from_string(Contract), + + {ok, {[], {{bytes, [4]}, _, _}}} = aaci_lookup_spec(AACI, "fixed_bytes"), + {ok, {[], {{bytes, [any]}, _, _}}} = aaci_lookup_spec(AACI, "any_bytes"), + {ok, {[], {bits, _, _}}} = aaci_lookup_spec(AACI, "bits"), + {ok, {[], {char, _, _}}} = aaci_lookup_spec(AACI, "character"), + + {ok, {[], {{"option", [integer]}, _, {variant, [{"None", []}, {"Some", [_]}]}}}} = aaci_lookup_spec(AACI, "options"), + {ok, {[], {"hash", _, {bytes, [32]}}}} = aaci_lookup_spec(AACI, "hash"), + {ok, {[], {"unit", _, {tuple, []}}}} = aaci_lookup_spec(AACI, "unit"), + + {ok, {_, {"Chain.ttl", _, {variant, _}}}} = aaci_lookup_spec(AACI, "ttl"), + {ok, {_, {"Chain.paying_for_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "paying_for"), + {ok, {_, {"Chain.ga_meta_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "ga_meta_tx"), + {ok, {_, {"Chain.base_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "base_tx"), + {ok, {_, {"Chain.tx", _, {record, _}}}} = aaci_lookup_spec(AACI, "tx"), + + {ok, {_, {"AENS.pointee", _, {variant, _}}}} = aaci_lookup_spec(AACI, "pointee"), + {ok, {_, {"AENS.name", _, {variant, _}}}} = aaci_lookup_spec(AACI, "name"), + {ok, {_, {"AENSv2.pointee", _, {variant, _}}}} = aaci_lookup_spec(AACI, "pointee2"), + {ok, {_, {"AENSv2.name", _, {variant, _}}}} = aaci_lookup_spec(AACI, "name2"), + + {ok, {_, {"MCL_BLS12_381.fr", _, {bytes, [32]}}}} = aaci_lookup_spec(AACI, "fr"), + {ok, {_, {"MCL_BLS12_381.fp", _, {bytes, [48]}}}} = aaci_lookup_spec(AACI, "fp"), + + {ok, {[], {{"Set.set", [integer]}, _, {record, [{"to_map", _}]}}}} = aaci_lookup_spec(AACI, "set"), + + ok. + +name_coerce_test() -> + AddrSoph = "ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx", + AddrFate = {address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123, + 167,208,53,78,40,235,2,163,132,36,47,183,228,151,9, + 210,39,214>>}, + {ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()), + TTLSoph = {"FixedTTL", 0}, + TTLFate = {variant, [1, 1], 0, {0}}, + try_coerce(TTL, TTLSoph, TTLFate), + {ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()), + PointeeSoph = {"AccountPt", AddrSoph}, + PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}}, + try_coerce(Pointee, PointeeSoph, PointeeFate), + {ok, Name} = annotate_type("AENS.name", builtin_typedefs()), + NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}}, + NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}}, + try_coerce(Name, NameSoph, NameFate). + +void_coerce_test() -> + % Void itself can't be represented, but other types built out of void are + % valid. + {ok, NonOption} = annotate_type({"option", ["void"]}, builtin_typedefs()), + try_coerce(NonOption, {"None"}, {variant, [0, 1], 0, {}}), + {ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()), + try_coerce(NonList, [], []). + -- 2.30.2 From f770bc299ed88adfaae6b3249f69b28c1b8079a2 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Thu, 9 Oct 2025 09:46:45 +1100 Subject: [PATCH 2/3] add {raw, binary()} case for all chain objects --- src/hz.erl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index d255c24..df9663b 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -1855,9 +1855,11 @@ coerce({_, _, contract}, {contract, Bin}, from_fate) -> Address = gmser_api_encoder:encode(contract_pubkey, Bin), {ok, unicode:characters_to_list(Address)}; coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) -> - % If it is a binary of 64 bytes then it can be used as is... If it is an - % sg_... string of 64 bytes, then it is too short to be valid, so just - % interpret it as a binary directly. + % Usually to pass a binary in, you need to wrap it as {raw, Binary}, but + % since sg_... strings OR hex blobs can be used as signatures in Sophia, we + % special case this case based on the length. Even if a binary starts with + % "sg_", 64 characters is not enough to represent a 64 byte signature, so + % the most optimistic interpretation is to use the binary directly. {ok, S}; coerce({O, N, signature}, S, to_fate) -> coerce_chain_object(O, N, signature, signature, S); @@ -1973,6 +1975,8 @@ coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> coerce_bytes(_, _, _, Bytes) -> {ok, Bytes}. +coerce_chain_object(_, _, _, _, {raw, Binary}) -> + {ok, Binary}; coerce_chain_object(O, N, T, Tag, S) -> case decode_chain_object(Tag, S) of {ok, Data} -> {ok, coerce_chain_object2(T, Data)}; @@ -2563,6 +2567,7 @@ coerce_signature_binary_test() -> 169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222, 249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114, 233,110,119,143,206,137,124,36,63,154,85,7>>, + {ok, Binary} = coerce(Type, {raw, Binary}, to_fate), {ok, Binary} = coerce(Type, Binary, to_fate), ok. -- 2.30.2 From 7c2db6eab750a34814f300317fa25082042afa10 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sat, 11 Oct 2025 10:10:39 +0900 Subject: [PATCH 3/3] Patch verup and fix TTL typespec --- ebin/hakuzaru.app | 2 +- src/hakuzaru.erl | 2 +- src/hz.erl | 6 +++--- src/hz_fetcher.erl | 2 +- src/hz_grids.erl | 2 +- src/hz_key_master.erl | 2 +- src/hz_man.erl | 2 +- src/hz_sup.erl | 2 +- zomp.meta | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ebin/hakuzaru.app b/ebin/hakuzaru.app index 685be51..0d71ca7 100644 --- a/ebin/hakuzaru.app +++ b/ebin/hakuzaru.app @@ -3,7 +3,7 @@ {included_applications,[]}, {applications,[stdlib,kernel]}, {description,"Gajumaru interoperation library"}, - {vsn,"0.6.1"}, + {vsn,"0.6.2"}, {modules,[hakuzaru,hz,hz_fetcher,hz_grids,hz_key_master,hz_man, hz_sup]}, {mod,{hakuzaru,[]}}]}. diff --git a/src/hakuzaru.erl b/src/hakuzaru.erl index 379c0b5..5f89478 100644 --- a/src/hakuzaru.erl +++ b/src/hakuzaru.erl @@ -6,7 +6,7 @@ %%% @end -module(hakuzaru). --vsn("0.6.1"). +-vsn("0.6.2"). -author("Craig Everett "). -copyright("Craig Everett "). -license("GPL-3.0-or-later"). diff --git a/src/hz.erl b/src/hz.erl index df9663b..9ea9454 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -23,7 +23,7 @@ %%% @end -module(hz). --vsn("0.6.1"). +-vsn("0.6.2"). -author("Craig Everett "). -copyright("Craig Everett "). -license("GPL-3.0-or-later"). @@ -890,7 +890,7 @@ contract_create(CreatorID, Path, InitArgs) -> when CreatorID :: pubkey(), Nonce :: pos_integer(), Amount :: non_neg_integer(), - TTL :: pos_integer(), + TTL :: non_neg_integer(), Gas :: pos_integer(), GasPrice :: pos_integer(), Path :: file:filename(), @@ -1226,7 +1226,7 @@ contract_call(CallerID, Gas, AACI, ConID, Fun, Args) -> Gas :: pos_integer(), GasPrice :: pos_integer(), Amount :: non_neg_integer(), - TTL :: pos_integer(), + TTL :: non_neg_integer(), AACI :: aaci(), ConID :: unicode:chardata(), Fun :: string(), diff --git a/src/hz_fetcher.erl b/src/hz_fetcher.erl index 421ef67..7f196b5 100644 --- a/src/hz_fetcher.erl +++ b/src/hz_fetcher.erl @@ -1,5 +1,5 @@ -module(hz_fetcher). --vsn("0.6.1"). +-vsn("0.6.2"). -author("Craig Everett "). -copyright("Craig Everett "). -license("MIT"). diff --git a/src/hz_grids.erl b/src/hz_grids.erl index be3ec03..4a9107f 100644 --- a/src/hz_grids.erl +++ b/src/hz_grids.erl @@ -37,7 +37,7 @@ %%% @end -module(hz_grids). --vsn("0.6.1"). +-vsn("0.6.2"). -export([url/2, parse/1, req/2, req/3]). diff --git a/src/hz_key_master.erl b/src/hz_key_master.erl index c880ee1..ceed8f1 100644 --- a/src/hz_key_master.erl +++ b/src/hz_key_master.erl @@ -8,7 +8,7 @@ %%% @end -module(hz_key_master). --vsn("0.6.1"). +-vsn("0.6.2"). -export([make_key/1, encode/1, decode/1]). diff --git a/src/hz_man.erl b/src/hz_man.erl index cb8bf23..c6f31a7 100644 --- a/src/hz_man.erl +++ b/src/hz_man.erl @@ -9,7 +9,7 @@ %%% @end -module(hz_man). --vsn("0.6.1"). +-vsn("0.6.2"). -behavior(gen_server). -author("Craig Everett "). -copyright("Craig Everett "). diff --git a/src/hz_sup.erl b/src/hz_sup.erl index 793cd15..187d2de 100644 --- a/src/hz_sup.erl +++ b/src/hz_sup.erl @@ -9,7 +9,7 @@ %%% @end -module(hz_sup). --vsn("0.6.1"). +-vsn("0.6.2"). -behaviour(supervisor). -author("Craig Everett "). -copyright("Craig Everett "). diff --git a/zomp.meta b/zomp.meta index f798ddc..f41a3b4 100644 --- a/zomp.meta +++ b/zomp.meta @@ -2,9 +2,9 @@ {type,app}. {modules,[]}. {prefix,"hz"}. -{desc,"Gajumaru interoperation library"}. {author,"Craig Everett"}. -{package_id,{"otpr","hakuzaru",{0,6,1}}}. +{desc,"Gajumaru interoperation library"}. +{package_id,{"otpr","hakuzaru",{0,6,2}}}. {deps,[{"otpr","sophia",{9,0,0}}, {"otpr","gmserialization",{0,1,3}}, {"otpr","gmbytecode",{3,4,1}}, -- 2.30.2