From 51bae6173639f8ba10065b271acf6daf4e705aa1 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Wed, 10 Apr 2024 16:34:29 +0200 Subject: [PATCH] Add signature literals (#505) * Add signature literals + handle system alias types * Add tests for signature literals + encode/decode * Add to CHANGELOG * Add in documentation * Additional documentation --- CHANGELOG.md | 4 ++++ docs/sophia_features.md | 2 +- docs/sophia_syntax.md | 2 ++ src/aeso_aci.erl | 3 ++- src/aeso_ast_infer_types.erl | 14 +++++++++-- src/aeso_ast_to_fcode.erl | 2 ++ src/aeso_compiler.erl | 25 ++++++++++++------- src/aeso_fcode_to_fate.erl | 1 + src/aeso_parser.erl | 2 +- src/aeso_pretty.erl | 3 ++- src/aeso_syntax.erl | 1 + src/aeso_vm_decode.erl | 3 +++ test/aeso_calldata_tests.erl | 1 + test/aeso_encode_decode_tests.erl | 40 +++++++++++++++++++++++++++++++ test/contracts/ceres.aes | 3 ++- test/contracts/funargs.aes | 2 ++ 16 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 test/aeso_encode_decode_tests.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eda1e2..43c877b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Support for OTP-27 - no changes in behavior. +- Signature literals `sg_...` - they have type `signature` (which is an alias for `bytes(64)`). ### Changed +- System aliases are handled explicitly when converting to a Sophia value, this is only + observable for `signature` where a value of type `signature` is now represented as a + (new) signature literal. ### Removed ### Fixed - Allow self-qualification, i.e. referencing `X.foo` when in namespace `X`. diff --git a/docs/sophia_features.md b/docs/sophia_features.md index 13e47cb..25abd28 100644 --- a/docs/sophia_features.md +++ b/docs/sophia_features.md @@ -573,7 +573,7 @@ Sophia has the following types: | state | `state{ owner = Call.origin, magic_key = #a298105f }` | | event | `EventX(0, "Hello")` | | hash | `#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f` | -| signature | `#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f` | +| signature | `sg_MhibzTP1wWzGCTjtPFr1TiPqRJrrJqw7auvEuF5i3FdoALWqXLBDY6xxRRNUSPHK3EQTnTzF12EyspkxrSMxVHKsZeSMj`, `#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f` | | Chain.ttl | `FixedTTL(1050)`, `RelativeTTL(50)` | | oracle('a, 'b) | `ok_2YNyxd6TRJPNrTcEDCe9ra59SVUdp9FR9qWC5msKZWYD9bP9z5` | | oracle_query('a, 'b) | `oq_2oRvyowJuJnEkxy58Ckkw77XfWJrmRgmGaLzhdqb67SKEL1gPY` | diff --git a/docs/sophia_syntax.md b/docs/sophia_syntax.md index 712c9ce..5b975ab 100644 --- a/docs/sophia_syntax.md +++ b/docs/sophia_syntax.md @@ -30,6 +30,7 @@ interface main using as for hiding - `ContractAddress` base58-encoded 32 byte contract address with `ct_` prefix - `OracleAddress` base58-encoded 32 byte oracle address with `ok_` prefix - `OracleQueryId` base58-encoded 32 byte oracle query id with `oq_` prefix +- `Signature` base58-encoded 64 byte cryptographic signature with `sg_` prefix Valid string escape codes are @@ -239,6 +240,7 @@ Expr ::= '(' LamArgs ')' '=>' Block(Stmt) // Anonymous function (x) => x + | Int | Bytes | String | Char // Literals 123, 0xff, #00abc123, "foo", '%' | AccountAddress | ContractAddress // Chain identifiers | OracleAddress | OracleQueryId // Chain identifiers + | Signature // Signature | '???' // Hole expression 1 + ??? Generator ::= Pattern '<-' Expr // Generator diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 5cf5aff..e4e6d2f 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -198,7 +198,8 @@ encode_expr({bytes, _, B}) -> <> = B, list_to_binary(lists:flatten(io_lib:format("#~*.16.0b", [Digits*2, N]))); encode_expr({Lit, _, L}) when Lit == oracle_pubkey; Lit == oracle_query_id; - Lit == contract_pubkey; Lit == account_pubkey -> + Lit == contract_pubkey; Lit == account_pubkey; + Lit == signature -> aeser_api_encoder:encode(Lit, L); encode_expr({app, _, {'-', _}, [{int, _, N}]}) -> encode_expr({int, [], -N}); diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index ca8b212..b78ea38 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -1935,6 +1935,8 @@ infer_expr(_Env, Body={bytes, As, Bin}) -> {typed, As, Body, {bytes_t, As, byte_size(Bin)}}; infer_expr(_Env, Body={account_pubkey, As, _}) -> {typed, As, Body, {id, As, "address"}}; +infer_expr(_Env, Body={signature, As, Bin}) when byte_size(Bin) == 64 -> + {typed, As, Body, {bytes_t, As, 64}}; infer_expr(_Env, Body={oracle_pubkey, As, _}) -> Q = fresh_uvar(As), R = fresh_uvar(As), @@ -2173,6 +2175,8 @@ check_valid_const_expr({bytes, _, _}) -> true; check_valid_const_expr({account_pubkey, _, _}) -> true; +check_valid_const_expr({signature, _, _}) -> + true; check_valid_const_expr({oracle_pubkey, _, _}) -> true; check_valid_const_expr({oracle_query_id, _, _}) -> @@ -3060,7 +3064,8 @@ unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id) unfold_types_in_type(Env, Id, Options) when ?is_type_id(Id) -> %% Like the case above, but for types without parameters. when_warning(warn_unused_typedefs, fun() -> used_typedef(Id, 0) end), - UnfoldRecords = proplists:get_value(unfold_record_types, Options, false), + UnfoldSysAlias = not proplists:get_value(not_unfold_system_alias_types, Options, false), + UnfoldRecords = proplists:get_value(unfold_record_types, Options, false), UnfoldVariants = proplists:get_value(unfold_variant_types, Options, false), case lookup_type(Env, Id) of {_, {_, {[], {record_t, Fields}}}} when UnfoldRecords -> @@ -3068,7 +3073,12 @@ unfold_types_in_type(Env, Id, Options) when ?is_type_id(Id) -> {_, {_, {[], {variant_t, Constrs}}}} when UnfoldVariants -> {variant_t, unfold_types_in_type(Env, Constrs, Options)}; {_, {_, {[], {alias_t, Type1}}}} -> - unfold_types_in_type(Env, Type1, Options); + case aeso_syntax:get_ann(Type1) of + [{origin, system}] when not UnfoldSysAlias -> + Id; + _ -> + unfold_types_in_type(Env, Type1, Options) + end; _ -> %% Not a record type, or ill-formed record type Id diff --git a/src/aeso_ast_to_fcode.erl b/src/aeso_ast_to_fcode.erl index 2e8c841..4ad2af9 100644 --- a/src/aeso_ast_to_fcode.erl +++ b/src/aeso_ast_to_fcode.erl @@ -55,6 +55,7 @@ | {contract_pubkey, binary()} | {oracle_pubkey, binary()} | {oracle_query_id, binary()} + | {signature, binary()} | {bool, false | true} | {contract_code, string()} %% for CREATE, by name | {typerep, ftype()}. @@ -599,6 +600,7 @@ expr_to_fcode(_Env, _Type, {char, Ann, N}) -> {lit, to_fann(Ann), {in expr_to_fcode(_Env, _Type, {bool, Ann, B}) -> {lit, to_fann(Ann), {bool, B}}; expr_to_fcode(_Env, _Type, {string, Ann, S}) -> {lit, to_fann(Ann), {string, S}}; expr_to_fcode(_Env, _Type, {account_pubkey, Ann, K}) -> {lit, to_fann(Ann), {account_pubkey, K}}; +expr_to_fcode(_Env, _Type, {signature, Ann, K}) -> {lit, to_fann(Ann), {signature, K}}; expr_to_fcode(_Env, _Type, {contract_pubkey, Ann, K}) -> {lit, to_fann(Ann), {contract_pubkey, K}}; expr_to_fcode(_Env, _Type, {oracle_pubkey, Ann, K}) -> {lit, to_fann(Ann), {oracle_pubkey, K}}; expr_to_fcode(_Env, _Type, {oracle_query_id, Ann, K}) -> {lit, to_fann(Ann), {oracle_query_id, K}}; diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 5968081..1cc72c3 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -228,10 +228,13 @@ encode_value(Contract0, Type, Value, Options) -> decode_value(Contract0, Type, FateValue, Options) -> case add_extra_call(Contract0, {type, Type}, Options) of {ok, CallName, Code} -> - #{ unfolded_typed_ast := TypedAst - , type_env := TypeEnv} = Code, + #{ folded_typed_ast := TypedAst + , type_env := TypeEnv} = Code, {ok, _, Type0} = get_decode_type(CallName, TypedAst), - Type1 = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, [unfold_record_types, unfold_variant_types]), + Type1 = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, + [ unfold_record_types + , unfold_variant_types + , not_unfold_system_alias_types ]), fate_data_to_sophia_value(Type0, Type1, FateValue); Err = {error, _} -> Err @@ -272,7 +275,7 @@ insert_call_function(Ast, Code, Call, {type, Type}) -> [ Code, "\n\n", lists:duplicate(Ind, " "), - "entrypoint ", Call, "(val : ", Type, ") = val\n" + "entrypoint ", Call, "(val : ", Type, ") : ", Type, " = val\n" ]). -spec insert_init_function(string(), options()) -> string(). @@ -311,9 +314,12 @@ to_sophia_value(ContractString, FunName, ok, Data, Options0) -> Options = [no_code | Options0], try Code = string_to_code(ContractString, Options), - #{ unfolded_typed_ast := TypedAst, type_env := TypeEnv} = Code, + #{ folded_typed_ast := TypedAst, type_env := TypeEnv} = Code, {ok, _, Type0} = get_decode_type(FunName, TypedAst), - Type = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, [unfold_record_types, unfold_variant_types]), + Type = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, + [ unfold_record_types + , unfold_variant_types + , not_unfold_system_alias_types]), fate_data_to_sophia_value(Type0, Type, Data) catch @@ -360,14 +366,17 @@ decode_calldata(ContractString, FunName, Calldata, Options0) -> Options = [no_code | Options0], try Code = string_to_code(ContractString, Options), - #{ unfolded_typed_ast := TypedAst, type_env := TypeEnv} = Code, + #{ folded_typed_ast := TypedAst, type_env := TypeEnv} = Code, {ok, Args, _} = get_decode_type(FunName, TypedAst), GetType = fun({typed, _, _, T}) -> T; (T) -> T end, ArgTypes = lists:map(GetType, Args), Type0 = {tuple_t, [], ArgTypes}, %% user defined data types such as variants needed to match against - Type = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, [unfold_record_types, unfold_variant_types]), + Type = aeso_ast_infer_types:unfold_types_in_type(TypeEnv, Type0, + [ unfold_record_types + , unfold_variant_types + , not_unfold_system_alias_types]), case aeb_fate_abi:decode_calldata(FunName, Calldata) of {ok, FateArgs} -> try diff --git a/src/aeso_fcode_to_fate.erl b/src/aeso_fcode_to_fate.erl index 3328d36..e0fbe16 100644 --- a/src/aeso_fcode_to_fate.erl +++ b/src/aeso_fcode_to_fate.erl @@ -235,6 +235,7 @@ lit_to_fate(Env, L) -> {bytes, B} -> aeb_fate_data:make_bytes(B); {bool, B} -> aeb_fate_data:make_boolean(B); {account_pubkey, K} -> aeb_fate_data:make_address(K); + {signature, S} -> aeb_fate_data:make_bytes(S); {contract_pubkey, K} -> aeb_fate_data:make_contract(K); {oracle_pubkey, K} -> aeb_fate_data:make_oracle(K); {oracle_query_id, H} -> aeb_fate_data:make_oracle_query(H); diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index 85bcf95..24e154d 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -525,7 +525,7 @@ id_or_addr() -> ?RULE(id(), parse_addr_literal(_1)). parse_addr_literal(Id = {id, Ann, Name}) -> - case lists:member(lists:sublist(Name, 3), ["ak_", "ok_", "oq_", "ct_"]) of + case lists:member(lists:sublist(Name, 3), ["ak_", "ok_", "oq_", "ct_", "sg_"]) of false -> Id; true -> try aeser_api_encoder:decode(list_to_binary(Name)) of diff --git a/src/aeso_pretty.erl b/src/aeso_pretty.erl index cb0c05d..effe80f 100644 --- a/src/aeso_pretty.erl +++ b/src/aeso_pretty.erl @@ -387,7 +387,8 @@ expr_p(_, {Type, _, Bin}) when Type == account_pubkey; Type == contract_pubkey; Type == oracle_pubkey; - Type == oracle_query_id -> + Type == oracle_query_id; + Type == signature -> text(binary_to_list(aeser_api_encoder:encode(Type, Bin))); expr_p(_, {string, _, <<>>}) -> text("\"\""); expr_p(_, {string, _, S}) -> diff --git a/src/aeso_syntax.erl b/src/aeso_syntax.erl index 1e8ee2d..cfa79b5 100644 --- a/src/aeso_syntax.erl +++ b/src/aeso_syntax.erl @@ -100,6 +100,7 @@ | {contract_pubkey, ann(), binary()} | {oracle_pubkey, ann(), binary()} | {oracle_query_id, ann(), binary()} + | {signature, ann(), binary()} | {string, ann(), binary()} | {char, ann(), integer()}. diff --git a/src/aeso_vm_decode.erl b/src/aeso_vm_decode.erl index f158c8f..b0e2f78 100644 --- a/src/aeso_vm_decode.erl +++ b/src/aeso_vm_decode.erl @@ -12,6 +12,9 @@ -spec from_fate(aeso_syntax:type(), aeb_fate_data:fate_type()) -> aeso_syntax:expr(). from_fate({id, _, "address"}, ?FATE_ADDRESS(Bin)) -> {account_pubkey, [], Bin}; +from_fate({id, _, "signature"}, ?FATE_BYTES(Bin)) -> {signature, [], Bin}; +from_fate({id, _, "hash"}, ?FATE_BYTES(Bin)) -> {bytes, [], Bin}; +from_fate({id, _, "unit"}, ?FATE_UNIT) -> {tuple, [], []}; from_fate({app_t, _, {id, _, "oracle"}, _}, ?FATE_ORACLE(Bin)) -> {oracle_pubkey, [], Bin}; from_fate({app_t, _, {id, _, "oracle_query"}, _}, ?FATE_ORACLE_Q(Bin)) -> {oracle_query_id, [], Bin}; from_fate({con, _, _Name}, ?FATE_CONTRACT(Bin)) -> {contract_pubkey, [], Bin}; diff --git a/test/aeso_calldata_tests.erl b/test/aeso_calldata_tests.erl index 3eb5af5..a49efd6 100644 --- a/test/aeso_calldata_tests.erl +++ b/test/aeso_calldata_tests.erl @@ -117,6 +117,7 @@ compilable_contracts() -> {"funargs", "chain_base_tx", ["Chain.NameRevokeTx(#ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)"]}, {"funargs", "chain_base_tx", ["Chain.NameTransferTx(ak_2dATVcZ9KJU5a8hdsVtTv21pYiGWiPbmVcU1Pz72FFqpk9pSRR, #ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)"]}, {"funargs", "chain_base_tx", ["Chain.GAAttachTx"]}, + {"funargs", "sig", ["sg_MhibzTP1wWzGCTjtPFr1TiPqRJrrJqw7auvEuF5i3FdoALWqXLBDY6xxRRNUSPHK3EQTnTzF12EyspkxrSMxVHKsZeSMj"]}, {"variant_types", "init", []}, {"basic_auth", "init", []}, {"address_literals", "init", []}, diff --git a/test/aeso_encode_decode_tests.erl b/test/aeso_encode_decode_tests.erl new file mode 100644 index 0000000..898842f --- /dev/null +++ b/test/aeso_encode_decode_tests.erl @@ -0,0 +1,40 @@ +-module(aeso_encode_decode_tests). + +-compile([export_all, nowarn_export_all]). + +-include_lib("eunit/include/eunit.hrl"). + +-define(EMPTY, "contract C =\n entrypoint f() = true"). + +encode_decode_test_() -> + [ {lists:flatten(io_lib:format("Testing encode-decode roundtrip for ~p : ~p", [Value, {EType, DType}])), + fun() -> + {ok, SerRes} = aeso_compiler:encode_value(?EMPTY, EType, Value, []), + {ok, Expr} = aeso_compiler:decode_value(?EMPTY, DType, SerRes, []), + Value2 = prettypr:format(aeso_pretty:expr(Expr)), + ?assertEqual(Value, Value2) + end} || {Value, EType, DType} <- test_data() ]. + +test_data() -> + lists:map(fun({V, T}) -> {V, T, T}; + ({V, T1, T2}) -> {V, T1, T2} end, data()). + +data() -> + [ {"42", "int"} + , {"- 42", "int"} + , {"true", "bool"} + , {"ak_Ez6MyeTMm17YnTnDdHTSrzMEBKmy7Uz2sXu347bTDPgVH2ifJ", "address"} + , {"ct_Ez6MyeTMm17YnTnDdHTSrzMEBKmy7Uz2sXu347bTDPgVH2ifJ", "C"} + , {"Some(42)", "option(int)"} + , {"None", "option(int)"} + , {"(true, 42)", "bool * int"} + , {"{[1] = true, [3] = false}", "map(int, bool)"} + , {"()", "unit"} + , {"#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "hash"} + , {"#000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "bytes(32)"} + , {"sg_MhibzTP1wWzGCTjtPFr1TiPqRJrrJqw7auvEuF5i3FdoALWqXLBDY6xxRRNUSPHK3EQTnTzF12EyspkxrSMxVHKsZeSMj", "signature"} + , {"sg_MhibzTP1wWzGCTjtPFr1TiPqRJrrJqw7auvEuF5i3FdoALWqXLBDY6xxRRNUSPHK3EQTnTzF12EyspkxrSMxVHKsZeSMj", "bytes(64)", "signature"} + , {"#0102030405060708090a0b0c0d0e0f101718192021222324252627282930313233343536373839401a1b1c1d1e1f202122232425262728293031323334353637", "bytes(64)"} + , {"#0102030405060708090a0b0c0d0e0f101718192021222324252627282930313233343536373839401a1b1c1d1e1f202122232425262728293031323334353637", "signature", "bytes(64)"} + ]. + diff --git a/test/contracts/ceres.aes b/test/contracts/ceres.aes index 832334f..7e869cf 100644 --- a/test/contracts/ceres.aes +++ b/test/contracts/ceres.aes @@ -11,4 +11,5 @@ contract C = let i = Int.mulmod(a, b, h) let j = Crypto.poseidon(i, a) let k : bytes(32) = Address.to_bytes(Call.origin) - (a bor b band c bxor a << bnot b >> a, k) + let l = sg_MhibzTP1wWzGCTjtPFr1TiPqRJrrJqw7auvEuF5i3FdoALWqXLBDY6xxRRNUSPHK3EQTnTzF12EyspkxrSMxVHKsZeSMj + (a bor b band c bxor a << bnot b >> a, k, l) diff --git a/test/contracts/funargs.aes b/test/contracts/funargs.aes index 32f0f0b..5d16112 100644 --- a/test/contracts/funargs.aes +++ b/test/contracts/funargs.aes @@ -59,3 +59,5 @@ contract FunctionArguments = entrypoint chain_ga_meta_tx(tx : Chain.ga_meta_tx) = true entrypoint chain_paying_for_tx(tx : Chain.paying_for_tx) = true entrypoint chain_base_tx(tx : Chain.base_tx) = true + + entrypoint sig(sg : signature) = true