6 Commits

Author SHA1 Message Date
Jarvis Carroll 68a0c3d623 coerce character
It's really just an integer... Should we flatten it to an integer
instead? I don't know.
2025-09-23 16:00:56 +10:00
Jarvis Carroll d3fb598506 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.
2025-09-23 15:42:53 +10:00
Jarvis Carroll 4ed2bd0cd1 coerce bytes 2025-09-23 14:35:42 +10:00
Jarvis Carroll 0caf5a61c7 coerce stringy booleans 2025-09-23 12:44:20 +10:00
Jarvis Carroll 7704d82c6f 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
2025-09-23 12:39:07 +10:00
Jarvis Carroll a6f58a95e2 add more atoms to AACI 2025-09-18 23:31:11 +10:00
+136 -26
View File
@@ -1520,20 +1520,39 @@ 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 builtins, strings (lists) for user-defined types.
%
% There are some magic built in types that may or may not also need atoms to
% represent them, and may or may not need to be handled explicitly in
% coerce/3, if we can't flatten them directly
%
% These types represent some FATE variant:
% Chain.ttl, AENS.pointee, AENS.name, AENSv2.pointee, AENSv2.name,
% Chain.ga_meta_tx, Chain.paying_for_tx, Chain.base_tx,
%
% And then MCL_BLS12_381.fr represent bytes(32), and MCL_BLS12_381.fp
% represents bytes(48).
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(<<"hash">>) -> hash;
opaque_type_name(<<"signature">>) -> signature;
opaque_type_name(<<"contract">>) -> contract;
opaque_type_name(<<"list">>) -> list;
opaque_type_name(<<"map">>) -> map;
opaque_type_name(<<"option">>) -> option;
opaque_type_name(<<"name">>) -> name;
opaque_type_name(<<"channel">>) -> channel;
opaque_type_name(Name) -> binary_to_list(Name).
% 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
@@ -1597,6 +1616,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}};
@@ -1788,33 +1811,28 @@ 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({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({_, _, 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 +1848,18 @@ coerce({O, N, string}, Str, Direction) ->
StrBin ->
{ok, StrBin}
end;
coerce({_, _, char}, Val, _Direction) when is_integer(Val) ->
{ok, Val};
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),
<<IntValue:Size>> = 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 +1909,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 +2469,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 +2493,15 @@ 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_bool_test() ->
{ok, Type} = annotate_type(boolean, #{}),
try_coerce(Type, true, true),
@@ -2463,6 +2534,18 @@ 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, $?, $?).
%%% Complex AACI paramter and namespace tests
@@ -2549,3 +2632,30 @@ 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 = "
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'
",
{ok, AACI} = aaci_from_string(Contract),
IntAnnotated = {integer, already_normalized, integer},
OptionFlat = {variant, [{"None", []}, {"Some", [IntAnnotated]}]},
OptionAnnotated = {{option, [integer]}, already_normalized, OptionFlat},
{ok, {[], OptionAnnotated}} = aaci_lookup_spec(AACI, "options"),
{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.