diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index a2ec0bc..d780e49 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -23,6 +23,7 @@ , decode_calldata/4 , parse/2 , add_include_path/2 + , validate_byte_code/3 ]). -include_lib("aebytecode/include/aeb_opcodes.hrl"). @@ -558,6 +559,87 @@ pp(Code, Options, Option, PPFun) -> ok end. +%% -- Byte code validation --------------------------------------------------- + +-define(protect(Tag, Code), fun() -> try Code catch _:Err1 -> throw({Tag, Err1}) end end()). + +-spec validate_byte_code(map(), string(), options()) -> ok | {error, [aeso_errors:error()]}. +validate_byte_code(#{ byte_code := ByteCode, payable := Payable }, Source, Options) -> + Fail = fun(Err) -> {error, [aeso_errors:new(data_error, Err)]} end, + case proplists:get_value(backend, Options, aevm) of + B when B /= fate -> Fail(io_lib:format("Unsupported backend: ~s\n", [B])); + fate -> + try + FCode1 = ?protect(deserialize, aeb_fate_code:strip_init_function(aeb_fate_code:deserialize(ByteCode))), + {FCode2, SrcPayable} = + ?protect(compile, + begin + {ok, #{ byte_code := SrcByteCode, payable := SrcPayable }} = + from_string1(fate, Source, Options), + FCode = aeb_fate_code:deserialize(SrcByteCode), + {aeb_fate_code:strip_init_function(FCode), SrcPayable} + end), + case compare_fate_code(FCode1, FCode2) of + ok when SrcPayable /= Payable -> + Not = fun(true) -> ""; (false) -> " not" end, + Fail(io_lib:format("Byte code contract is~s payable, but source code contract is~s.\n", + [Not(Payable), Not(SrcPayable)])); + ok -> ok; + {error, Why} -> Fail(io_lib:format("Byte code does not match source code.\n~s", [Why])) + end + catch + throw:{deserialize, _} -> Fail("Invalid byte code"); + throw:{compile, {error, Errs}} -> {error, Errs} + end + end. + +compare_fate_code(FCode1, FCode2) -> + Funs1 = aeb_fate_code:functions(FCode1), + Funs2 = aeb_fate_code:functions(FCode2), + Syms1 = aeb_fate_code:symbols(FCode1), + Syms2 = aeb_fate_code:symbols(FCode2), + FunHashes1 = maps:keys(Funs1), + FunHashes2 = maps:keys(Funs2), + case FunHashes1 == FunHashes2 of + false -> + InByteCode = [ binary_to_list(maps:get(H, Syms1)) || H <- FunHashes1 -- FunHashes2 ], + InSourceCode = [ binary_to_list(maps:get(H, Syms2)) || H <- FunHashes2 -- FunHashes1 ], + Msg = [ io_lib:format("- Functions in the byte code but not in the source code:\n" + " ~s\n", [string:join(InByteCode, ", ")]) || InByteCode /= [] ] ++ + [ io_lib:format("- Functions in the source code but not in the byte code:\n" + " ~s\n", [string:join(InSourceCode, ", ")]) || InSourceCode /= [] ], + {error, Msg}; + true -> + case lists:append([ compare_fate_fun(maps:get(H, Syms1), Fun1, Fun2) + || {{H, Fun1}, {_, Fun2}} <- lists:zip(maps:to_list(Funs1), + maps:to_list(Funs2)) ]) of + [] -> ok; + Errs -> {error, Errs} + end + end. + +compare_fate_fun(_Name, Fun, Fun) -> []; +compare_fate_fun(Name, {Attr, Type, _}, {Attr, Type, _}) -> + [io_lib:format("- The implementation of the function ~s is different.\n", [Name])]; +compare_fate_fun(Name, {Attr1, Type, _}, {Attr2, Type, _}) -> + [io_lib:format("- The attributes of the function ~s differ:\n" + " Byte code: ~s\n" + " Source code: ~s\n", + [Name, string:join([ atom_to_list(A) || A <- Attr1 ], ", "), + string:join([ atom_to_list(A) || A <- Attr2 ], ", ")])]; +compare_fate_fun(Name, {_, Type1, _}, {_, Type2, _}) -> + [io_lib:format("- The type of the function ~s differs:\n" + " Byte code: ~s\n" + " Source code: ~s\n", + [Name, pp_fate_sig(Type1), pp_fate_sig(Type2)])]. + +pp_fate_sig({[Arg], Res}) -> + io_lib:format("~s => ~s", [pp_fate_type(Arg), pp_fate_type(Res)]); +pp_fate_sig({Args, Res}) -> + io_lib:format("(~s) => ~s", [string:join([pp_fate_type(Arg) || Arg <- Args], ", "), pp_fate_type(Res)]). + +pp_fate_type(T) -> io_lib:format("~w", [T]). + %% ------------------------------------------------------------------- sophia_type_to_typerep(String) -> diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index afa4e6a..50130e6 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -12,6 +12,14 @@ -include_lib("eunit/include/eunit.hrl"). +run_test(Test) -> + TestFun = list_to_atom(lists:concat([Test, "_test_"])), + [ begin + io:format("~s\n", [Label]), + Fun() + end || {Label, Fun} <- ?MODULE:TestFun() ], + ok. + %% Very simply test compile the given contracts. Only basic checks %% are made on the output, just that it is a binary which indicates %% that the compilation worked. @@ -702,3 +710,44 @@ failing_code_gen_contracts() -> "The state cannot contain functions in the AEVM. Use FATE if you need this.") ]. +validation_test_() -> + [{"Validation fail: " ++ C1 ++ " /= " ++ C2, + fun() -> + Actual = case validate(C1, C2) of + {error, Errs} -> Errs; + ok -> #{} + end, + check_errors(Expect, Actual) + end} || {C1, C2, Expect} <- validation_fails()] ++ + [{"Validation of " ++ C, + fun() -> + ?assertEqual(ok, validate(C, C)) + end} || C <- compilable_contracts()]. + +validation_fails() -> + [{"deadcode", "nodeadcode", + [<<"Data error:\n" + "Byte code does not match source code.\n" + "- Functions in the source code but not in the byte code:\n" + " .MyList.map2">>]}, + {"validation_test1", "validation_test2", + [<<"Data error:\n" + "Byte code does not match source code.\n" + "- The implementation of the function code_fail is different.\n" + "- The attributes of the function attr_fail differ:\n" + " Byte code: payable\n" + " Source code: \n" + "- The type of the function type_fail differs:\n" + " Byte code: integer => integer\n" + " Source code: {tvar,0} => {tvar,0}">>]}, + {"validation_test1", "validation_test3", + [<<"Data error:\n" + "Byte code contract is not payable, but source code contract is.">>]}]. + +validate(Contract1, Contract2) -> + ByteCode = #{ fate_code := FCode } = compile(fate, Contract1), + FCode1 = aeb_fate_code:serialize(aeb_fate_code:strip_init_function(FCode)), + Source = aeso_test_utils:read_contract(Contract2), + aeso_compiler:validate_byte_code(ByteCode#{ byte_code := FCode1 }, Source, + [{backend, fate}, {include, {file_system, [aeso_test_utils:contract_path()]}}]). + diff --git a/test/contracts/validation_test1.aes b/test/contracts/validation_test1.aes new file mode 100644 index 0000000..75b43d1 --- /dev/null +++ b/test/contracts/validation_test1.aes @@ -0,0 +1,4 @@ +contract ValidationTest = + payable entrypoint attr_fail() = () + entrypoint type_fail(x : int) = x + entrypoint code_fail(x) = x + 1 diff --git a/test/contracts/validation_test2.aes b/test/contracts/validation_test2.aes new file mode 100644 index 0000000..f77334e --- /dev/null +++ b/test/contracts/validation_test2.aes @@ -0,0 +1,4 @@ +contract ValidationTest = + entrypoint attr_fail() = () + entrypoint type_fail(x) = x + entrypoint code_fail(x) = x - 1 diff --git a/test/contracts/validation_test3.aes b/test/contracts/validation_test3.aes new file mode 100644 index 0000000..36af27d --- /dev/null +++ b/test/contracts/validation_test3.aes @@ -0,0 +1,4 @@ +payable contract ValidationTest = + payable entrypoint attr_fail() = () + entrypoint type_fail(x : int) = x + entrypoint code_fail(x) = x + 1