Compare commits

..

4 Commits

Author SHA1 Message Date
SpiveeWorks
f54a33a293 Fix type substitution into variants and records
Variants were working by accident, since
{variant, [{"VariantName", [Element]}]} had a similar enough form to
the opaque types that would come from something like
`type1(type2(int))`, but records were not working, since they have a
different form. Now both are handled explicitly so that only the
intended forms of each are handled.
2025-01-24 19:14:16 +11:00
SpiveeWorks
1d71c16d6a Also prepare AACI for namespace types 2025-01-24 19:14:16 +11:00
SpiveeWorks
28dba962af Even more unit tests
Trying to test all the basic types that coerce covers, and a couple more
type parameter and nested cases.
2025-01-24 19:14:16 +11:00
SpiveeWorks
6e335067f9 Add unit tests for some simple coercions 2025-01-24 19:14:15 +11:00
3 changed files with 159 additions and 194 deletions

View File

@ -657,13 +657,13 @@ dry_run_map(Map) ->
%% @doc %% @doc
%% Decode the "cb_XXXX" string that came out of a tx_info or dry_run, to %% Decode the "cb_XXXX" string that came out of a tx_info or dry_run, to
%% the Erlang representation of FATE objects used by gmb_fate_encoding. See %% the Erlang representation of FATE objects used by aeb_fate_encoding. See
%% decode_bytearray/2 for an alternative that provides simpler outputs based on %% decode_bytearray/2 for an alternative that provides simpler outputs based on
%% information provided by an AACI. %% information provided by an AACI.
decode_bytearray_fate(EncodedStr) -> decode_bytearray_fate(EncodedStr) ->
Encoded = unicode:characters_to_binary(EncodedStr), Encoded = unicode:characters_to_binary(EncodedStr),
{contract_bytearray, Binary} = gmser_api_encoder:decode(Encoded), {contract_bytearray, Binary} = aeser_api_encoder:decode(Encoded),
case Binary of case Binary of
<<>> -> {ok, none}; <<>> -> {ok, none};
<<"Out of gas">> -> {error, out_of_gas}; <<"Out of gas">> -> {error, out_of_gas};
@ -671,7 +671,7 @@ decode_bytearray_fate(EncodedStr) ->
% FIXME there may be other errors that are encoded directly into % FIXME there may be other errors that are encoded directly into
% the byte array. We could try and catch to at least return % the byte array. We could try and catch to at least return
% *something* for cases that we don't already detect. % *something* for cases that we don't already detect.
Object = gmb_fate_encoding:deserialize(Binary), Object = aeb_fate_encoding:deserialize(Binary),
{ok, Object} {ok, Object}
end. end.
@ -953,7 +953,7 @@ contract_create(CreatorID, Path, InitArgs) ->
%% </li> %% </li>
%% <li> %% <li>
%% <b>GasPrice:</b> %% <b>GasPrice:</b>
%% This is a factor that is used calculate a value in pucks (the smallest unit of %% This is a factor that is used calculate a value in aettos (the smallest unit of
%% Gajumaru's currency value) for the gas consumed. In times of high contention %% Gajumaru's currency value) for the gas consumed. In times of high contention
%% in the mempool increasing the gas price increases the value of mining a given %% in the mempool increasing the gas price increases the value of mining a given
%% transaction, thus making miners more likely to prioritize the high value ones. %% transaction, thus making miners more likely to prioritize the high value ones.
@ -1059,14 +1059,14 @@ contract_create3(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArg
contract_create4(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) -> contract_create4(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
PK = unicode:characters_to_binary(CreatorID), PK = unicode:characters_to_binary(CreatorID),
try try
{account_pubkey, OwnerID} = gmser_api_encoder:decode(PK), {account_pubkey, OwnerID} = aeser_api_encoder:decode(PK),
contract_create5(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) contract_create5(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData)
catch catch
Error:Reason -> {Error, Reason} Error:Reason -> {Error, Reason}
end. end.
contract_create5(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) -> contract_create5(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
Code = gmser_contract_code:serialize(Compiled), Code = aeser_contract_code:serialize(Compiled),
Source = maps:get(contract_source, Compiled, <<>>), Source = maps:get(contract_source, Compiled, <<>>),
VM = 1, VM = 1,
ABI = 1, ABI = 1,
@ -1074,7 +1074,7 @@ contract_create5(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData)
ContractCreateVersion = 1, ContractCreateVersion = 1,
Type = contract_create_tx, Type = contract_create_tx,
Fields = Fields =
[{owner_id, gmser_id:create(account, OwnerID)}, [{owner_id, aeser_id:create(account, OwnerID)},
{nonce, Nonce}, {nonce, Nonce},
{code, Code}, {code, Code},
{source, Source}, {source, Source},
@ -1097,9 +1097,9 @@ contract_create5(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData)
{gas_price, int}, {gas_price, int},
{gas, int}, {gas, int},
{call_data, binary}], {call_data, binary}],
TXB = gmser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields), TXB = aeser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields),
try try
{ok, gmser_api_encoder:encode(transaction, TXB)} {ok, aeser_api_encoder:encode(transaction, TXB)}
catch catch
error:Reason -> {error, Reason} error:Reason -> {error, Reason}
end. end.
@ -1272,7 +1272,7 @@ contract_call(CallerID, Gas, AACI, ConID, Fun, Args) ->
%% </li> %% </li>
%% <li> %% <li>
%% <b>GasPrice:</b> %% <b>GasPrice:</b>
%% This is a factor that is used calculate a value in pucks (the smallest unit of %% This is a factor that is used calculate a value in aettos (the smallest unit of
%% Gajumaru's currency value) for the gas consumed. In times of high contention %% Gajumaru's currency value) for the gas consumed. In times of high contention
%% in the mempool increasing the gas price increases the value of mining a given %% in the mempool increasing the gas price increases the value of mining a given
%% transaction, thus making miners more likely to prioritize the high value ones. %% transaction, thus making miners more likely to prioritize the high value ones.
@ -1330,7 +1330,7 @@ contract_call(CallerID, Nonce, Gas, GP, Amount, TTL, AACI, ConID, Fun, Args) ->
contract_call2(CallerID, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) -> contract_call2(CallerID, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) ->
CallerBin = unicode:characters_to_binary(CallerID), CallerBin = unicode:characters_to_binary(CallerID),
try try
{account_pubkey, PK} = gmser_api_encoder:decode(CallerBin), {account_pubkey, PK} = aeser_api_encoder:decode(CallerBin),
contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData)
catch catch
Error:Reason -> {Error, Reason} Error:Reason -> {Error, Reason}
@ -1339,7 +1339,7 @@ contract_call2(CallerID, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) ->
contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) -> contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) ->
ConBin = unicode:characters_to_binary(ConID), ConBin = unicode:characters_to_binary(ConID),
try try
{contract_pubkey, CK} = gmser_api_encoder:decode(ConBin), {contract_pubkey, CK} = aeser_api_encoder:decode(ConBin),
contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData)
catch catch
Error:Reason -> {Error, Reason} Error:Reason -> {Error, Reason}
@ -1350,9 +1350,9 @@ contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) ->
CallVersion = 1, CallVersion = 1,
Type = contract_call_tx, Type = contract_call_tx,
Fields = Fields =
[{caller_id, gmser_id:create(account, PK)}, [{caller_id, aeser_id:create(account, PK)},
{nonce, Nonce}, {nonce, Nonce},
{contract_id, gmser_id:create(contract, CK)}, {contract_id, aeser_id:create(contract, CK)},
{abi_version, ABI}, {abi_version, ABI},
{ttl, TTL}, {ttl, TTL},
{amount, Amount}, {amount, Amount},
@ -1369,9 +1369,9 @@ contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) ->
{gas_price, int}, {gas_price, int},
{gas, int}, {gas, int},
{call_data, binary}], {call_data, binary}],
TXB = gmser_chain_objects:serialize(Type, CallVersion, Template, Fields), TXB = aeser_chain_objects:serialize(Type, CallVersion, Template, Fields),
try try
{ok, gmser_api_encoder:encode(transaction, TXB)} {ok, aeser_api_encoder:encode(transaction, TXB)}
catch catch
error:Reason -> {error, Reason} error:Reason -> {error, Reason}
end. end.
@ -1386,106 +1386,105 @@ contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) ->
%% of calldata %% of calldata
prepare_contract(File) -> prepare_contract(File) ->
case gmso_compiler:file(File, [{aci, json}]) of case aeso_compiler:file(File, [{aci, json}]) of
{ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)}; {ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)};
Error -> Error Error -> Error
end. end.
prepare_aaci(ACI) -> prepare_aaci(ACI) ->
% We want to take the types represented by the ACI, things like N1.T(N2.T), Types = lists:foldl(fun prepare_namespace_types/2, #{}, ACI),
% and dereference them down to concrete types like
% {tuple, [integer, string]}. Our type dereferencing algorithms
% shouldn't act directly on the JSON-based structures that the compiler
% gives us, though, though, so before we do the analysis, we should strip
% the ACI down to a list of 'opaque' type defintions and function specs.
{Name, OpaqueSpecs, TypeDefs} = convert_aci_types(ACI),
% Now that we have the opaque types, we can dereference the function specs
% 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, #{}),
{aaci, Name, Specs, TypeDefs}.
convert_aci_types(ACI) ->
% Find the main contract, so we can get the specifications of its
% entrypoints.
[{NameBin, SpecDefs}] = [{NameBin, SpecDefs}] =
[{N, F} [{N, F}
|| #{contract := #{kind := contract_main, || #{contract := #{kind := contract_main,
functions := F, functions := F,
name := N}} <- ACI], name := N}} <- ACI],
Name = binary_to_list(NameBin), Name = binary_to_list(NameBin),
% Turn these specifications into opaque types that we can reason about. Specs = simplify_specs(SpecDefs, #{}, Types),
Specs = lists:map(fun convert_function_spec/1, SpecDefs), {aaci, Name, Specs, Types}.
% These specifications can reference other type definitions from the main prepare_namespace_types(#{namespace := NS}, Types) ->
% contract and any other namespaces, so extract these types and convert prepare_namespace_types2(NS, false, Types);
% them too. prepare_namespace_types(#{contract := NS}, Types) ->
TypeDefTree = lists:map(fun convert_namespace_typedefs/1, ACI), prepare_namespace_types2(NS, true, Types).
% The tree structure of the ACI naturally leads to a tree of opaque types,
% but we want a map, so flatten it out before we continue.
TypeDefMap = collect_opaque_types(TypeDefTree, #{}),
% This is all the information we actually need from the ACI, the rest is prepare_namespace_types2(NS, IsContract, Types) ->
% just pre-compute and acceleration. TypeDefs = maps:get(typedefs, NS),
{Name, Specs, TypeDefMap}. NameBin = maps:get(name, NS),
convert_function_spec(#{name := NameBin, arguments := Args, returns := Result}) ->
Name = binary_to_list(NameBin), Name = binary_to_list(NameBin),
ArgTypes = lists:map(fun convert_arg/1, Args), Types2 = case IsContract of
ResultType = opaque_type([], Result), true ->
{Name, ArgTypes, ResultType}. maps:put(Name, {[], contract}, Types);
false ->
convert_arg(#{name := NameBin, type := TypeDef}) -> Types
Name = binary_to_list(NameBin), end,
{ok, Type} = opaque_type([], TypeDef), Types3 = case maps:find(state, NS) of
{Name, Type}. {ok, StateDefACI} ->
convert_namespace_typedefs(#{namespace := NS}) ->
Name = namespace_name(NS),
convert_typedefs(NS, Name);
convert_namespace_typedefs(#{contract := NS}) ->
Name = namespace_name(NS),
ImplicitTypes = convert_implicit_types(NS, Name),
ExplicitTypes = convert_typedefs(NS, Name),
[ImplicitTypes, ExplicitTypes].
namespace_name(#{name := NameBin}) ->
binary_to_list(NameBin).
convert_implicit_types(#{state := StateDefACI}, Name) ->
StateDefOpaque = opaque_type([], StateDefACI), StateDefOpaque = opaque_type([], StateDefACI),
[{Name, [], contract}, maps:put(Name ++ ".state", {[], StateDefOpaque}, Types2);
{Name ++ ".state", [], StateDefOpaque}]; error ->
convert_implicit_types(_, Name) -> Types2
[{Name, [], contract}]. end,
simplify_typedefs(TypeDefs, Types3, Name ++ ".").
convert_typedefs(#{typedefs := TypeDefs}, Name) -> simplify_typedefs([], Types, _NamePrefix) ->
convert_typedefs_loop(TypeDefs, Name ++ ".", []). Types;
simplify_typedefs([Next | Rest], Types, NamePrefix) ->
% Take a namespace that has already had a period appended, and use that as a #{name := NameBin, vars := ParamDefs, typedef := T} = Next,
% prefix to convert and annotate a list of types.
convert_typedefs_loop([], _NamePrefix, Converted) ->
Converted;
convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
#{name := NameBin, vars := ParamDefs, typedef := DefACI} = Next,
Name = NamePrefix ++ binary_to_list(NameBin), Name = NamePrefix ++ binary_to_list(NameBin),
Params = [binary_to_list(Param) || #{name := Param} <- ParamDefs], Params = [binary_to_list(Param) || #{name := Param} <- ParamDefs],
Def = opaque_type(Params, DefACI), Type = opaque_type(Params, T),
convert_typedefs_loop(Rest, NamePrefix, [Converted, {Name, Params, Def}]). NewTypes = maps:put(Name, {Params, Type}, Types),
simplify_typedefs(Rest, NewTypes, NamePrefix).
collect_opaque_types([], Types) -> simplify_specs([], Specs, _Types) ->
Types; Specs;
collect_opaque_types([L | R], Types) -> simplify_specs([Next | Rest], Specs, Types) ->
NewTypes = collect_opaque_types(L, Types), #{name := NameBin, arguments := ArgDefs, returns := ResultDef} = Next,
collect_opaque_types(R, NewTypes); Name = binary_to_list(NameBin),
collect_opaque_types({Name, Params, Def}, Types) -> ArgTypes = [simplify_args(Arg, Types) || Arg <- ArgDefs],
maps:put(Name, {Params, Def}, Types). {ok, ResultType} = type(ResultDef, Types),
NewSpecs = maps:put(Name, {ArgTypes, ResultType}, Specs),
simplify_specs(Rest, NewSpecs, Types).
simplify_args(#{name := NameBin, type := TypeDef}, Types) ->
Name = binary_to_list(NameBin),
% FIXME We should make this error more informative, and continue
% propogating it up, so that the user can provide their own ACI and find
% out whether it worked or not. At that point ACI -> AACI could almost be a
% module or package of its own.
{ok, Type} = type(TypeDef, Types),
{Name, Type}.
% 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
% fate-esque erlang expressions that aebytecode can serialize. Second, we need
% partially substituted names, so that error messages can be generated for why
% "foobar" is not valid as the third field of a `bazquux`, because the third
% field is supposed to be `option(integer)`, not `string`.
%
% To achieve this we need three representations of each type expression, which
% together form an 'annotated type'. First, we need the fully opaque name,
% "bazquux", then we need the normalized name, which is an opaque name with the
% bare-minimum substitution needed to make the outer-most type-constructor an
% identifiable built-in, ADT, or record type, and then we need the flattened
% type, which is the raw {variant, [{Name, Fields}, ...]} or
% {record, [{Name, Type}]} expression that can be used in actual Sophia->FATE
% coercion. The type sub-expressions in these flattened types will each be
% fully annotated as well, i.e. they will each contain *all three* of the above
% representations, so that coercion of subexpressions remains fast AND
% informative.
%
% In a lot of cases the opaque type given will already be normalized, in which
% case either the normalized field or the non-normalized field of an annotated
% type can simple be the atom `already_normalized`, which means error messages
% can simply render the normalized type expression and know that the error will
% make sense.
type(T, Types) ->
O = opaque_type([], T),
flatten_opaque_type(O, Types).
% Convert an ACI type defintion/spec into the 'opaque type' representation that
% our dereferencing algorithms can reason about.
opaque_type(Params, NameBin) when is_binary(NameBin) -> opaque_type(Params, NameBin) when is_binary(NameBin) ->
Name = opaque_type_name(NameBin), Name = opaque_type_name(NameBin),
case not is_atom(Name) and lists:member(Name, Params) of case not is_atom(Name) and lists:member(Name, Params) of
@ -1509,7 +1508,7 @@ opaque_type(Params, Pair) when is_map(Pair) ->
[{Name, TypeArgs}] = maps:to_list(Pair), [{Name, TypeArgs}] = maps:to_list(Pair),
{opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}. {opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}.
% atoms for builtins, strings (lists) for user-defined types % atoms for builtins, lists for user defined types
opaque_type_name(<<"int">>) -> integer; opaque_type_name(<<"int">>) -> integer;
opaque_type_name(<<"address">>) -> address; opaque_type_name(<<"address">>) -> address;
opaque_type_name(<<"contract">>) -> contract; opaque_type_name(<<"contract">>) -> contract;
@ -1520,49 +1519,16 @@ opaque_type_name(<<"map">>) -> map;
opaque_type_name(<<"string">>) -> string; opaque_type_name(<<"string">>) -> string;
opaque_type_name(Name) -> binary_to_list(Name). opaque_type_name(Name) -> binary_to_list(Name).
% Type preparation has two goals. First, we need a data structure that can be flatten_opaque_type(T, Types) ->
% traversed quickly, to take sophia-esque erlang expressions and turn them into
% fate-esque erlang expressions that gmbytecode can serialize. Second, we need
% partially substituted names, so that error messages can be generated for why
% "foobar" is not valid as the third field of a `bazquux`, because the third
% field is supposed to be `option(integer)`, not `string`.
%
% To achieve this we need three representations of each type expression, which
% together form an 'annotated type'. First, we need the fully opaque name,
% "bazquux", then we need the normalized name, which is an opaque name with the
% bare-minimum substitution needed to make the outer-most type-constructor an
% identifiable built-in, ADT, or record type, and then we need the dereferenced
% type, which is the raw {variant, [{Name, Fields}, ...]} or
% {record, [{Name, Type}]} expression that can be used in actual Sophia->FATE
% coercion. The type sub-expressions in these dereferenced types will each be
% fully annotated as well, i.e. they will each contain *all three* of the above
% representations, so that coercion of subexpressions remains fast and
% informative.
%
% In a lot of cases the opaque type given will already be normalized, in which
% case either the normalized field or the non-normalized field of an annotated
% type can simple be the atom `already_normalized`, which means error messages
% can simply render the normalized type expression and know that the error will
% make sense.
annotate_function_specs([], _Types, Specs) ->
Specs;
annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs) ->
{ok, Args} = annotate_types(ArgsOpaque, Types, []),
{ok, Result} = annotate_type(ResultOpaque, Types),
NewSpecs = maps:put(Name, {Args, Result}, Specs),
annotate_function_specs(Rest, Types, NewSpecs).
annotate_type(T, Types) ->
case normalize_opaque_type(T, Types) of case normalize_opaque_type(T, Types) of
{ok, AlreadyNormalized, NOpaque, NExpanded} -> {ok, AlreadyNormalized, NOpaque, NExpanded} ->
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types); flatten_opaque_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types);
Error -> Error ->
Error Error
end. end.
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) -> flatten_opaque_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
case annotate_type_subexpressions(NExpanded, Types) of case flatten_normalized_type(NExpanded, Types) of
{ok, Flat} -> {ok, Flat} ->
case AlreadyNormalized of case AlreadyNormalized of
true -> {ok, {T, already_normalized, Flat}}; true -> {ok, {T, already_normalized, Flat}};
@ -1572,48 +1538,48 @@ annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
Error Error
end. end.
annotate_types([T | Rest], Types, Acc) -> flatten_opaque_types([T | Rest], Types, Acc) ->
case annotate_type(T, Types) of case flatten_opaque_type(T, Types) of
{ok, Type} -> annotate_types(Rest, Types, [Type | Acc]); {ok, Type} -> flatten_opaque_types(Rest, Types, [Type | Acc]);
Error -> Error Error -> Error
end; end;
annotate_types([], _Types, Acc) -> flatten_opaque_types([], _Types, Acc) ->
{ok, lists:reverse(Acc)}. {ok, lists:reverse(Acc)}.
annotate_type_subexpressions(PrimitiveType, _Types) when is_atom(PrimitiveType) -> flatten_opaque_bindings([{Name, T} | Rest], Types, Acc) ->
case flatten_opaque_type(T, Types) of
{ok, Type} -> flatten_opaque_bindings(Rest, Types, [{Name, Type} | Acc]);
Error -> Error
end;
flatten_opaque_bindings([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
flatten_opaque_variants([{Name, Elems} | Rest], Types, Acc) ->
case flatten_opaque_types(Elems, Types, []) of
{ok, ElemsFlat} -> flatten_opaque_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
Error -> Error
end;
flatten_opaque_variants([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
flatten_normalized_type(PrimitiveType, _Types) when is_atom(PrimitiveType) ->
{ok, PrimitiveType}; {ok, PrimitiveType};
annotate_type_subexpressions({variant, VariantsOpaque}, Types) -> flatten_normalized_type({variant, VariantsOpaque}, Types) ->
case annotate_variants(VariantsOpaque, Types, []) of case flatten_opaque_variants(VariantsOpaque, Types, []) of
{ok, Variants} -> {ok, {variant, Variants}}; {ok, Variants} -> {ok, {variant, Variants}};
Error -> Error Error -> Error
end; end;
annotate_type_subexpressions({record, FieldsOpaque}, Types) -> flatten_normalized_type({record, FieldsOpaque}, Types) ->
case annotate_bindings(FieldsOpaque, Types, []) of case flatten_opaque_bindings(FieldsOpaque, Types, []) of
{ok, Fields} -> {ok, {record, Fields}}; {ok, Fields} -> {ok, {record, Fields}};
Error -> Error Error -> Error
end; end;
annotate_type_subexpressions({T, ElemsOpaque}, Types) -> flatten_normalized_type({T, ElemsOpaque}, Types) ->
case annotate_types(ElemsOpaque, Types, []) of case flatten_opaque_types(ElemsOpaque, Types, []) of
{ok, Elems} -> {ok, {T, Elems}}; {ok, Elems} -> {ok, {T, Elems}};
Error -> Error Error -> Error
end. end.
annotate_bindings([{Name, T} | Rest], Types, Acc) ->
case annotate_type(T, Types) of
{ok, Type} -> annotate_bindings(Rest, Types, [{Name, Type} | Acc]);
Error -> Error
end;
annotate_bindings([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
annotate_variants([{Name, Elems} | Rest], Types, Acc) ->
case annotate_types(Elems, Types, []) of
{ok, ElemsFlat} -> annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
Error -> Error
end;
annotate_variants([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types) ->
case type_is_expanded(T) of case type_is_expanded(T) of
false -> normalize_opaque_type(T, Types, true); false -> normalize_opaque_type(T, Types, true);
@ -1774,7 +1740,7 @@ coerce({O, N, integer}, S, to_fate) when is_list(S) ->
end; end;
coerce({O, N, address}, S, to_fate) -> coerce({O, N, address}, S, to_fate) ->
try try
case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of
{account_pubkey, Key} -> {ok, {address, Key}}; {account_pubkey, Key} -> {ok, {address, Key}};
_ -> single_error({invalid, O, N, S}) _ -> single_error({invalid, O, N, S})
end end
@ -1782,11 +1748,11 @@ coerce({O, N, address}, S, to_fate) ->
error:_ -> single_error({invalid, O, N, S}) error:_ -> single_error({invalid, O, N, S})
end; end;
coerce({_, _, address}, {address, Bin}, from_fate) -> coerce({_, _, address}, {address, Bin}, from_fate) ->
Address = gmser_api_encoder:encode(account_pubkey, Bin), Address = aeser_api_encoder:encode(account_pubkey, Bin),
{ok, unicode:characters_to_list(Address)}; {ok, unicode:characters_to_list(Address)};
coerce({O, N, contract}, S, to_fate) -> coerce({O, N, contract}, S, to_fate) ->
try try
case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of
{contract_pubkey, Key} -> {ok, {contract, Key}}; {contract_pubkey, Key} -> {ok, {contract, Key}};
_ -> single_error({invalid, O, N, S}) _ -> single_error({invalid, O, N, S})
end end
@ -1794,7 +1760,7 @@ coerce({O, N, contract}, S, to_fate) ->
error:_ -> single_error({invalid, O, N, S}) error:_ -> single_error({invalid, O, N, S})
end; end;
coerce({_, _, contract}, {contract, Bin}, from_fate) -> coerce({_, _, contract}, {contract, Bin}, from_fate) ->
Address = gmser_api_encoder:encode(contract_pubkey, Bin), Address = aeser_api_encoder:encode(contract_pubkey, Bin),
{ok, unicode:characters_to_list(Address)}; {ok, unicode:characters_to_list(Address)};
coerce({_, _, boolean}, true, _) -> coerce({_, _, boolean}, true, _) ->
{ok, true}; {ok, true};
@ -2061,7 +2027,8 @@ aaci_lookup_spec({aaci, _, FunDefs, _}, Fun) ->
%% @doc %% @doc
%% This function always returns 1,000,000,000 in the current version. %% This function always returns 1,000,000,000 in the current version.
%% %%
%% This is the minimum gas price returned by aec_tx_pool:minimum_miner_gas_price() %% This is the minimum gas price returned by aec_tx_pool:minimum_miner_gas_price(),
%% (the default set in aeternity_config_schema.json).
%% %%
%% Surely there can be some more nuance to this, but until a "gas station" type %% Surely there can be some more nuance to this, but until a "gas station" type
%% market/chain survey service exists we will use this naive value as a default %% market/chain survey service exists we will use this naive value as a default
@ -2095,7 +2062,7 @@ encode_call_data({aaci, _ContractName, FunDefs, _TypeDefs}, Fun, Args) ->
encode_call_data2(ArgDef, Fun, Args) -> encode_call_data2(ArgDef, Fun, Args) ->
case coerce_bindings(ArgDef, Args, to_fate) of case coerce_bindings(ArgDef, Args, to_fate) of
{ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); {ok, Coerced} -> aeb_fate_abi:create_calldata(Fun, Coerced);
Errors -> Errors Errors -> Errors
end. end.
@ -2115,7 +2082,7 @@ encode_call_data2(ArgDef, Fun, Args) ->
%% check failed before verification was able to pass or fail (bad key encoding or similar). %% check failed before verification was able to pass or fail (bad key encoding or similar).
verify_signature(Sig, Message, PubKey) -> verify_signature(Sig, Message, PubKey) ->
case gmser_api_encoder:decode(PubKey) of case aeser_api_encoder:decode(PubKey) of
{account_pubkey, PK} -> verify_signature2(Sig, Message, PK); {account_pubkey, PK} -> verify_signature2(Sig, Message, PK);
Other -> {error, {bad_key, Other}} Other -> {error, {bad_key, Other}}
end. end.
@ -2124,11 +2091,20 @@ verify_signature2(Sig, Message, PK) ->
% Gajumaru signatures require messages to be salted and hashed, then % Gajumaru signatures require messages to be salted and hashed, then
% the hash is what gets signed in order to protect % the hash is what gets signed in order to protect
% the user from accidentally signing a transaction disguised as a message. % the user from accidentally signing a transaction disguised as a message.
%
% Salt the message then hash with blake2b. See:
% 1. Erlang Blake2 blake2b/2 function:
% https://gitlab.com/ioecs/eblake2/blob/60a079f00d72d1bfcc25de8e6996d28f912db3fd/src/eblake2.erl#L23-L25
% 2. SDK salting step:
% https://gitlab.com/ioecs/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L171-L175
% 3. SDK hashing:
% https://gitlab.com/ioecs/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L83-L85
Prefix = <<"Gajumaru Signed Message:\n">>, Prefix = <<"Gajumaru Signed Message:\n">>,
{ok, PSize} = vencode(byte_size(Prefix)), {ok, PSize} = vencode(byte_size(Prefix)),
{ok, MSize} = vencode(byte_size(Message)), {ok, MSize} = vencode(byte_size(Message)),
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]), Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
{ok, Hashed} = eblake2:blake2b(32, Smashed), {ok, Hashed} = eblake2:blake2b(32, Smashed),
% Signature = <<(binary_to_integer(Sig, 16)):(64 * 8)>>,
Signature = base64:decode(Sig), Signature = base64:decode(Sig),
Result = ecu_eddsa:sign_verify_detached(Signature, Hashed, PK), Result = ecu_eddsa:sign_verify_detached(Signature, Hashed, PK),
{ok, Result}. {ok, Result}.
@ -2218,11 +2194,11 @@ try_coerce(Type, Sophia, Fate) ->
ok. ok.
coerce_int_test() -> coerce_int_test() ->
{ok, Type} = annotate_type(integer, #{}), {ok, Type} = flatten_opaque_type(integer, #{}),
try_coerce(Type, 123, 123). try_coerce(Type, 123, 123).
coerce_address_test() -> coerce_address_test() ->
{ok, Type} = annotate_type(address, #{}), {ok, Type} = flatten_opaque_type(address, #{}),
try_coerce(Type, try_coerce(Type,
"ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx", "ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123, {address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
@ -2230,7 +2206,7 @@ coerce_address_test() ->
210,39,214>>}). 210,39,214>>}).
coerce_contract_test() -> coerce_contract_test() ->
{ok, Type} = annotate_type(contract, #{}), {ok, Type} = flatten_opaque_type(contract, #{}),
try_coerce(Type, try_coerce(Type,
"ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx", "ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{contract, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123, {contract, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
@ -2238,35 +2214,35 @@ coerce_contract_test() ->
210,39,214>>}). 210,39,214>>}).
coerce_bool_test() -> coerce_bool_test() ->
{ok, Type} = annotate_type(boolean, #{}), {ok, Type} = flatten_opaque_type(boolean, #{}),
try_coerce(Type, true, true), try_coerce(Type, true, true),
try_coerce(Type, false, false). try_coerce(Type, false, false).
coerce_string_test() -> coerce_string_test() ->
{ok, Type} = annotate_type(string, #{}), {ok, Type} = flatten_opaque_type(string, #{}),
try_coerce(Type, "hello world", <<"hello world">>). try_coerce(Type, "hello world", <<"hello world">>).
coerce_list_test() -> coerce_list_test() ->
{ok, Type} = annotate_type({list, [string]}, #{}), {ok, Type} = flatten_opaque_type({list, [string]}, #{}),
try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]). try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]).
coerce_map_test() -> coerce_map_test() ->
{ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}), {ok, Type} = flatten_opaque_type({map, [string, {list, [integer]}]}, #{}),
try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}). try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}).
coerce_tuple_test() -> coerce_tuple_test() ->
{ok, Type} = annotate_type({tuple, [integer, string]}, #{}), {ok, Type} = flatten_opaque_type({tuple, [integer, string]}, #{}),
try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}). try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
coerce_variant_test() -> coerce_variant_test() ->
{ok, Type} = annotate_type({variant, [{"A", [integer]}, {ok, Type} = flatten_opaque_type({variant, [{"A", [integer]},
{"B", [integer, integer]}]}, {"B", [integer, integer]}]},
#{}), #{}),
try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}), try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}). try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
coerce_record_test() -> coerce_record_test() ->
{ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}), {ok, Type} = flatten_opaque_type({record, [{"a", integer}, {"b", integer}]}, #{}),
try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).

View File

@ -6,6 +6,8 @@
-export([connect/4, slowly_connect/4]). -export([connect/4, slowly_connect/4]).
-include("$zx_include/zx_logger.hrl").
connect(Node = {Host, Port}, Request, From, Timeout) -> connect(Node = {Host, Port}, Request, From, Timeout) ->
Timer = erlang:send_after(Timeout, self(), timeout), Timer = erlang:send_after(Timeout, self(), timeout),
@ -234,10 +236,3 @@ url({Node, Port}, Path) when is_list(Node) ->
["https://", Node, ":", integer_to_list(Port), Path]; ["https://", Node, ":", integer_to_list(Port), Path];
url({Node, Port}, Path) when is_tuple(Node) -> url({Node, Port}, Path) when is_tuple(Node) ->
["https://", inet:ntoa(Node), ":", integer_to_list(Port), Path]. ["https://", inet:ntoa(Node), ":", integer_to_list(Port), Path].
log(Level, Format, Args) ->
Raw = io_lib:format("~w ~w: " ++ Format, [?MODULE, self() | Args]),
Entry = unicode:characters_to_list(Raw),
logger:log(Level, Entry).

View File

@ -29,6 +29,8 @@
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]). code_change/3, terminate/2]).
%% TODO: Make logging more flexible
-include("$zx_include/zx_logger.hrl").
%%% Type and Record Definitions %%% Type and Record Definitions
@ -195,7 +197,7 @@ handle_info({'DOWN', Mon, process, PID, Info}, State) ->
NewState = handle_down(PID, Mon, Info, State), NewState = handle_down(PID, Mon, Info, State),
{noreply, NewState}; {noreply, NewState};
handle_info(Unexpected, State) -> handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), ok = log("Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}. {noreply, State}.
@ -285,11 +287,3 @@ do_request(Request,
do_request(Request, From, State = #s{chain_nodes = {[], Used}}) -> do_request(Request, From, State = #s{chain_nodes = {[], Used}}) ->
Fresh = lists:reverse(Used), Fresh = lists:reverse(Used),
do_request(Request, From, State#s{chain_nodes = {Fresh, []}}). do_request(Request, From, State#s{chain_nodes = {Fresh, []}}).
log(Level, Format, Args) ->
Raw = io_lib:format("~w ~w: " ++ Format, [?MODULE, self() | Args]),
Entry = unicode:characters_to_list(Raw),
logger:log(Level, Entry).