From d3cac6a644b1cd190b09a29c2f3e4ceb0ed1312d Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Mon, 12 May 2025 10:34:31 -0400 Subject: [PATCH 01/29] Implement call wrapper to simplify making sync calls --- libraries/eosiolib/contracts/eosio/action.hpp | 80 +------------------ libraries/eosiolib/contracts/eosio/call.hpp | 51 ++++++++++++ libraries/eosiolib/contracts/eosio/detail.hpp | 79 ++++++++++++++++++ 3 files changed, 131 insertions(+), 79 deletions(-) create mode 100644 libraries/eosiolib/contracts/eosio/detail.hpp diff --git a/libraries/eosiolib/contracts/eosio/action.hpp b/libraries/eosiolib/contracts/eosio/action.hpp index d043931e25..331ddb70d0 100644 --- a/libraries/eosiolib/contracts/eosio/action.hpp +++ b/libraries/eosiolib/contracts/eosio/action.hpp @@ -6,11 +6,11 @@ #include #include +#include "detail.hpp" #include "../../core/eosio/serialize.hpp" #include "../../core/eosio/datastream.hpp" #include "../../core/eosio/name.hpp" #include "../../core/eosio/fixed_bytes.hpp" -#include "../../core/eosio/ignore.hpp" #include "../../core/eosio/time.hpp" namespace eosio { @@ -409,84 +409,6 @@ namespace eosio { }; - - - namespace detail { - - /// @cond INTERNAL - - template - struct unwrap { typedef T type; }; - - template - struct unwrap> { typedef T type; }; - - template - auto get_args(R(Act::*p)(Args...)) { - return std::tuple::type>...>{}; - } - - template - auto get_args_nounwrap(R(Act::*p)(Args...)) { - return std::tuple...>{}; - } - - template - using deduced = decltype(get_args(Action)); - - template - using deduced_nounwrap = decltype(get_args_nounwrap(Action)); - - template - struct convert { typedef T type; }; - - template <> - struct convert { typedef std::string type; }; - - template <> - struct convert { typedef std::string type; }; - - template - struct is_same { static constexpr bool value = std::is_convertible::value; }; - - template - struct is_same { static constexpr bool value = std::is_integral::value; }; - - template - struct is_same { static constexpr bool value = std::is_integral::value; }; - - template - struct get_nth_impl { static constexpr auto value = get_nth_impl::value; }; - - template - struct get_nth_impl { static constexpr auto value = Arg; }; - - template - struct get_nth { static constexpr auto value = get_nth_impl::value; }; - - template - struct check_types { - static_assert(detail::is_same::type, typename convert>::type>::type>::value); - using type = check_types; - static constexpr bool value = true; - }; - template - struct check_types { - static_assert(detail::is_same::type, typename convert>::type>::type>::value); - static constexpr bool value = true; - }; - - template - constexpr bool type_check() { - static_assert(sizeof...(Ts) == std::tuple_size>::value); - if constexpr (sizeof...(Ts) != 0) - return check_types::value; - return true; - } - - /// @endcond - } - /** * Wrapper for an action object. * diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index ec1e0c7c63..5f02dc37cb 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -5,6 +5,7 @@ #include #include +#include "detail.hpp" #include "../../core/eosio/serialize.hpp" #include "../../core/eosio/datastream.hpp" #include "../../core/eosio/name.hpp" @@ -107,4 +108,54 @@ namespace eosio { } }; + /** + * Wrapper for a call object. + * + * @brief Used to wrap an a particular sync call to simplify the process of other contracts making sync calls to the "wrapped" call. + * Example: + * @code + * // defined by contract writer of the sync call functions + * using get_func = call_wrapper<"get"_n, &callee::get, uint32_t>; + * // usage by different contract writer + * get_func{"callee"_n}(); + * // or + * get_func get{"callee"_n}; + * get(); + * @endcode + */ + template + struct call_wrapper { + template + constexpr call_wrapper(Receiver&& receiver, bool read_only = false, bool no_op = false) + : receiver(std::forward(receiver)) + , read_only(read_only) + , no_op(no_op) + {} + + static constexpr eosio::name func_name = eosio::name(Func_Name); + eosio::name receiver {}; + bool read_only = false; + bool no_op = false; + + template + call to_call(Args&&... args)const { + static_assert(detail::type_check()); + return call(receiver, std::make_tuple(func_name, detail::deduced{std::forward(args)...}), read_only, no_op); + } + + template + Return_Type operator()(Args&&... args)const { + auto size = to_call(std::forward(args)...)(); + + if constexpr (std::is_void::value) { + return; + } else { + constexpr size_t max_stack_buffer_size = 512; + char* buffer = (char*)(max_stack_buffer_size < size ? malloc(size) : alloca(size)); + internal_use_do_not_use::get_call_return_value(buffer, size); + return unpack(buffer, size); + } + } + + }; } // namespace eosio diff --git a/libraries/eosiolib/contracts/eosio/detail.hpp b/libraries/eosiolib/contracts/eosio/detail.hpp new file mode 100644 index 0000000000..3261ac8030 --- /dev/null +++ b/libraries/eosiolib/contracts/eosio/detail.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "../../core/eosio/ignore.hpp" + +namespace eosio { namespace detail { + + /// @cond INTERNAL + + template + struct unwrap { typedef T type; }; + + template + struct unwrap> { typedef T type; }; + + template + auto get_args(R(Act::*p)(Args...)) { + return std::tuple::type>...>{}; + } + + template + auto get_args_nounwrap(R(Act::*p)(Args...)) { + return std::tuple...>{}; + } + + template + using deduced = decltype(get_args(Function)); + + template + using deduced_nounwrap = decltype(get_args_nounwrap(Function)); + + template + struct convert { typedef T type; }; + + template <> + struct convert { typedef std::string type; }; + + template <> + struct convert { typedef std::string type; }; + + template + struct is_same { static constexpr bool value = std::is_convertible::value; }; + + template + struct is_same { static constexpr bool value = std::is_integral::value; }; + + template + struct is_same { static constexpr bool value = std::is_integral::value; }; + + template + struct get_nth_impl { static constexpr auto value = get_nth_impl::value; }; + + template + struct get_nth_impl { static constexpr auto value = Arg; }; + + template + struct get_nth { static constexpr auto value = get_nth_impl::value; }; + + template + struct check_types { + static_assert(detail::is_same::type, typename convert>::type>::type>::value); + using type = check_types; + static constexpr bool value = true; + }; + template + struct check_types { + static_assert(detail::is_same::type, typename convert>::type>::type>::value); + static constexpr bool value = true; + }; + + template + constexpr bool type_check() { + static_assert(sizeof...(Ts) == std::tuple_size>::value); + if constexpr (sizeof...(Ts) != 0) + return check_types::value; + return true; + } + + /// @endcond +}} // eosio detail From 1c9cd279930eec9150df0de5a0987e01cabe1e8a Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Mon, 12 May 2025 10:35:22 -0400 Subject: [PATCH 02/29] tests/integration/call_tests.cpp --- .../unit/test_contracts/sync_call_callee.cpp | 40 ++++++++---------- .../unit/test_contracts/sync_call_callee.hpp | 24 +++++++++++ .../unit/test_contracts/sync_call_caller.cpp | 42 ++++++++++++++++--- 3 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 tests/unit/test_contracts/sync_call_callee.hpp diff --git a/tests/unit/test_contracts/sync_call_callee.cpp b/tests/unit/test_contracts/sync_call_callee.cpp index f149479e76..12720c1e78 100644 --- a/tests/unit/test_contracts/sync_call_callee.cpp +++ b/tests/unit/test_contracts/sync_call_callee.cpp @@ -1,28 +1,22 @@ -#include +#include "sync_call_callee.hpp" #include -#include -class [[eosio::contract]] sync_call_callee : public eosio::contract{ -public: - using contract::contract; +[[eosio::call]] +uint32_t sync_call_callee::getten() { + return 10; +} - [[eosio::call]] - uint32_t getten() { - return 10; - } +[[eosio::call]] +uint32_t sync_call_callee::getback(uint32_t in) { + return in; +} - [[eosio::call]] - uint32_t getback(uint32_t in) { - return in; - } +[[eosio::call]] +void sync_call_callee::voidfunc() { + eosio::print("I am a void function"); +} - [[eosio::call]] - void voidfunc() { - eosio::print("I am a void function"); - } - - [[eosio::action, eosio::call]] - uint32_t sum(uint32_t in1, uint32_t in2, uint32_t in3) { - return in1 + in2 + in3; - } -}; +[[eosio::action, eosio::call]] +uint32_t sync_call_callee::sum(uint32_t in1, uint32_t in2, uint32_t in3) { + return in1 + in2 + in3; +} diff --git a/tests/unit/test_contracts/sync_call_callee.hpp b/tests/unit/test_contracts/sync_call_callee.hpp new file mode 100644 index 0000000000..47a1d55d7d --- /dev/null +++ b/tests/unit/test_contracts/sync_call_callee.hpp @@ -0,0 +1,24 @@ +#include +#include + +class [[eosio::contract]] sync_call_callee : public eosio::contract{ +public: + using contract::contract; + + [[eosio::call]] + uint32_t getten(); + + [[eosio::call]] + uint32_t getback(uint32_t in); + + [[eosio::call]] + void voidfunc(); + + [[eosio::action, eosio::call]] + uint32_t sum(uint32_t in1, uint32_t in2, uint32_t in3); + + using getten_func = eosio::call_wrapper<"getten"_n, &sync_call_callee::getten, uint32_t>; + using getback_func = eosio::call_wrapper<"getback"_n, &sync_call_callee::getback, uint32_t>; + using voidfunc_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc>; + using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum, uint32_t>; +}; diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index b413e6909c..df076b12d1 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -1,3 +1,5 @@ +#include "sync_call_callee.hpp" + #include #include @@ -5,8 +7,9 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ public: using contract::contract; + // Using host function directly [[eosio::action]] - void retvaltest() { + void hstretvaltst() { auto expected_size = eosio::call("callee"_n, "getten"_n)(); eosio::check(expected_size >= 0, "call did not return a positive value"); @@ -17,8 +20,16 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ eosio::check(eosio::unpack(return_value) == 10u, "return value not 10"); // getten always returns 10 } + // Using call_wrapper + [[eosio::action]] + void wrpretvaltst() { + sync_call_callee::getten_func getten{ "callee"_n }; + eosio::check(getten() == 10u, "return value not 10"); + } + + // Using host function directly, testing one parameter passing [[eosio::action]] - void paramtest() { + void hstoneprmtst() { // `getback(uint32_t p)` returns p auto expected_size = eosio::call("callee"_n, std::make_tuple("getback"_n, 5))(); eosio::check(expected_size >= 0, "call did not return a positive value"); @@ -30,8 +41,16 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ eosio::check(eosio::unpack(return_value) == 5u, "return value not 5"); // getback returns back the same value of parameter } + // Using call_wrapper, testing one parameter passing + [[eosio::action]] + void wrponeprmtst() { + sync_call_callee::getback_func getback{ "callee"_n }; + eosio::check(getback(5) == 5u, "return value not 5"); + } + + // Using host function directly, testing multiple parameters passing [[eosio::action]] - void mulparamtest() { + void hstmulprmtst() { auto expected_size = eosio::call("callee"_n, std::make_tuple("sum"_n, 10, 20, 30))(); eosio::check(expected_size >= 0, "call did not return a positive value"); @@ -39,15 +58,28 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ return_value.resize(expected_size); auto actual_size = eosio::get_call_return_value(return_value.data(), return_value.size()); eosio::check(actual_size == expected_size, "actual_size not equal to expected_size"); - eosio::check(eosio::unpack(return_value) == 60u, "return value not 60"); // sum returns the sum of the 3 arguments + eosio::check(eosio::unpack(return_value) == 60u, "sum of 10, 20, an 30 not 60"); // sum returns the sum of the 3 arguments } + // Using call_wrapper, testing multiple parameters passing [[eosio::action]] - void voidfunctest() { + void wrpmulprmtst() { + sync_call_callee::sum_func sum{ "callee"_n }; + eosio::check(sum(10, 20, 30) == 60u, "sum of 10, 20, an 30 not 60"); + } + + [[eosio::action]] + void hstvodfuntst() { auto expected_size = eosio::call("callee"_n, "voidfunc"_n)(); eosio::check(expected_size == 0, "call did not return 0"); // void function. return value size should be 0 } + [[eosio::action]] + void wrpvodfuntst() { + sync_call_callee::voidfunc_func voidfunc{ "callee"_n }; + voidfunc(); + } + [[eosio::action]] void unknwnfuntst() { eosio::call("callee"_n, "unknwnfunc"_n)(); // unknwnfunc will never be in "callee"_n contract From f45496e4542e50ae482c6255bf8f55ddaca16a70 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Mon, 12 May 2025 13:23:19 -0400 Subject: [PATCH 03/29] Use execution_mode and on_call_not_supported_mode enums instead of bools for read_only and behavior when sync call is not supported by receiver --- libraries/eosiolib/contracts/eosio/call.hpp | 41 ++++++++------ tests/integration/call_tests.cpp | 53 +++++++++++++------ .../sync_call_not_supported.cpp | 4 +- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 5f02dc37cb..c4e1194243 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -47,6 +47,13 @@ namespace eosio { internal_use_do_not_use::set_call_return_value(mem, len); } + // Request a sync call is read_write or read_only. Default is read_write + enum execution_mode { read_write = 0, read_only = 1 }; + + // Behaviour of a sync call if the receiver does not support sync calls + // Default is abort + enum on_call_not_supported_mode { abort = 0, no_op = 1 }; + /** * This is the packed representation of a call * @@ -56,24 +63,24 @@ namespace eosio { /** * Name of the account the call is intended for */ - const name receiver{}; + name receiver{}; /** * indicating if the call is read only or not */ - const bool read_only = false; + execution_mode exec_mode = execution_mode::read_write; /** * if the receiver contract does not have sync_call entry point or its signature - * is invalid, when no_op_if_receiver_not_support_sync_call is set to true, + * is invalid, when on_call_not_supported_mode is set to no_op, * the sync call is no op, otherwise the call is aborted and an exception is raised. */ - const bool no_op_if_receiver_not_support_sync_call = false; + on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort; /** * Payload data */ - const std::vector data{}; + std::vector data{}; /** * Construct a new call object with receiver, name, and payload data @@ -84,25 +91,25 @@ namespace eosio { * @param payload - The call data that will be serialized via pack into data */ template - call( struct name receiver, T&& payload, bool read_only = false, bool no_op = false ) + call( struct name receiver, T&& payload, execution_mode exec_mode = execution_mode::read_write, on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort) : receiver(receiver) - , read_only(read_only) - , no_op_if_receiver_not_support_sync_call(no_op) + , exec_mode(exec_mode) + , not_supported_mode(not_supported_mode) , data(pack(std::forward(payload))) {} /// @cond INTERNAL - EOSLIB_SERIALIZE( call, (receiver)(read_only)(no_op_if_receiver_not_support_sync_call)(data) ) + EOSLIB_SERIALIZE( call, (receiver)(exec_mode)(not_supported_mode)(data) ) /// @endcond /** * Make a call using the functor operator */ int64_t operator()() const { - uint64_t flags = read_only ? 0x01 : 0x00; // last bit indicating read only + uint64_t flags = (exec_mode == execution_mode::read_only) ? 0x01 : 0x00; // last bit indicating read only auto retval = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); if (retval == -1) { // sync call is not supported by the receiver contract - check(no_op_if_receiver_not_support_sync_call, "receiver does not support sync call but no_op_if_receiver_not_support_sync_call flag is not set"); + check(not_supported_mode == on_call_not_supported_mode::no_op, "receiver does not support sync call but on_call_not_supported_mode is set to abort"); } return retval; } @@ -126,21 +133,21 @@ namespace eosio { template struct call_wrapper { template - constexpr call_wrapper(Receiver&& receiver, bool read_only = false, bool no_op = false) + constexpr call_wrapper(Receiver&& receiver, execution_mode exec_mode = execution_mode::read_write, on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort) : receiver(std::forward(receiver)) - , read_only(read_only) - , no_op(no_op) + , exec_mode(exec_mode) + , not_supported_mode(not_supported_mode) {} static constexpr eosio::name func_name = eosio::name(Func_Name); eosio::name receiver {}; - bool read_only = false; - bool no_op = false; + execution_mode exec_mode = execution_mode::read_write; + on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort; template call to_call(Args&&... args)const { static_assert(detail::type_check()); - return call(receiver, std::make_tuple(func_name, detail::deduced{std::forward(args)...}), read_only, no_op); + return call(receiver, std::make_tuple(func_name, detail::deduced{std::forward(args)...}), exec_mode, not_supported_mode); } template diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index f14d25f334..a39d69578c 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -41,7 +41,11 @@ BOOST_AUTO_TEST_CASE(return_value_test) { try { {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} }); - BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "retvaltest"_n, "caller"_n, {})); + // Using host function directly + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstretvaltst"_n, "caller"_n, {})); + + // Using call_wrapper + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "wrpretvaltst"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } // Verify one parameter passing works correctly @@ -51,7 +55,11 @@ BOOST_AUTO_TEST_CASE(param_basic_test) { try { {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} }); - BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "paramtest"_n, "caller"_n, {})); + // Using host function directly + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstoneprmtst"_n, "caller"_n, {})); + + // Using call_wrapper + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "wrponeprmtst"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } // Verify multiple parameters passing works correctly @@ -61,7 +69,11 @@ BOOST_AUTO_TEST_CASE(multiple_params_test) { try { {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} }); - BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "mulparamtest"_n, "caller"_n, {})); + // Using host function directly + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstmulprmtst"_n, "caller"_n, {})); + + // Using call_wrapper + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "wrpmulprmtst"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } // Verify a sync call to a void function works properly. @@ -71,18 +83,27 @@ BOOST_AUTO_TEST_CASE(void_func_test) { try { {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} }); - auto trx_trace = t.push_action("caller"_n, "voidfunctest"_n, "caller"_n, {}); - auto& atrace = trx_trace->action_traces; + auto check = [] (const transaction_trace_ptr& trx_trace) { + auto& atrace = trx_trace->action_traces; + + auto& call_traces = atrace[0].call_traces; + BOOST_REQUIRE_EQUAL(call_traces.size(), 1u); + + // Verify the print from the void function is correct. + // The test contract checks the return value size is 0. + auto& call_trace = call_traces[0]; + BOOST_REQUIRE_EQUAL(call_trace.call_ordinal, 1u); + BOOST_REQUIRE_EQUAL(call_trace.sender_ordinal, 0u); + BOOST_REQUIRE_EQUAL(call_trace.console, "I am a void function"); + }; - auto& call_traces = atrace[0].call_traces; - BOOST_REQUIRE_EQUAL(call_traces.size(), 1u); + // Using host function directly + auto trx_trace = t.push_action("caller"_n, "hstvodfuntst"_n, "caller"_n, {}); + check(trx_trace); - // Verify the print from the void function is correct. - // The test contract checks the return value size is 0. - auto& call_trace = call_traces[0]; - BOOST_REQUIRE_EQUAL(call_trace.call_ordinal, 1u); - BOOST_REQUIRE_EQUAL(call_trace.sender_ordinal, 0u); - BOOST_REQUIRE_EQUAL(call_trace.console, "I am a void function"); + // Using call_wrapper + trx_trace = t.push_action("caller"_n, "wrpvodfuntst"_n, "caller"_n, {}); + check(trx_trace); } FC_LOG_AND_RETHROW() } // Verify a function tagged as both `action` and `call` works @@ -96,7 +117,7 @@ BOOST_AUTO_TEST_CASE(mixed_action_call_tags_test) { try { // Make sure we can make a sync call to `sum` (`mulparamtest` in `caller` does // a sync call to `sum`) - BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "mulparamtest"_n, "caller"_n, {})); + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstmulprmtst"_n, "caller"_n, {})); // Make sure we can push an action using `sum`. BOOST_REQUIRE_NO_THROW(t.push_action("callee"_n, "sum"_n, "callee"_n, @@ -116,7 +137,7 @@ BOOST_AUTO_TEST_CASE(single_function_test) { try { // The single_func_wasm contains only one function and the caller contract // hooks up with it - BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "retvaltest"_n, "caller"_n, {})); + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstretvaltst"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } // Verify no_op_if_receiver_not_support_sync_call flag works @@ -135,7 +156,7 @@ BOOST_AUTO_TEST_CASE(sync_call_not_supported_test) { try { // so the call aborts BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "noopnotset"_n, "caller"_n, {}), eosio_assert_message_exception, - fc_exception_message_contains("receiver does not support sync call but no_op_if_receiver_not_support_sync_call flag is not set")); + fc_exception_message_contains("receiver does not support sync call but on_call_not_supported_mode is set to abort")); } FC_LOG_AND_RETHROW() } // Verify calling an unknown function will result in an eosio_assert diff --git a/tests/unit/test_contracts/sync_call_not_supported.cpp b/tests/unit/test_contracts/sync_call_not_supported.cpp index 4d07dcb460..e210160a5b 100644 --- a/tests/unit/test_contracts/sync_call_not_supported.cpp +++ b/tests/unit/test_contracts/sync_call_not_supported.cpp @@ -12,8 +12,8 @@ class [[eosio::contract]] sync_call_not_supported : public eosio::contract{ std::vector data{}; // For now, because sync_call entry point has not been implemented yet and - // no_op_if_receiver_no_support_sync_call is set to true, call should return -1 - auto rc = eosio::call("caller"_n, data, false /* read_only */, true /* no_op_if_receiver_no_support_sync_call */)(); + // on_call_not_supported_mode is set no_op, call should return -1 + auto rc = eosio::call("caller"_n, data, eosio::execution_mode::read_write, eosio::on_call_not_supported_mode::no_op)(); eosio::check(rc == -1, "call did not return -1"); // call was not executed. return value size should be 0 From 7454073875cdcf4d22ced040176b7fdb6ecaaacf Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Mon, 12 May 2025 13:24:37 -0400 Subject: [PATCH 04/29] Change parameter names of test function sum --- tests/integration/call_tests.cpp | 6 +++--- tests/unit/test_contracts/sync_call_callee.cpp | 4 ++-- tests/unit/test_contracts/sync_call_callee.hpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index a39d69578c..db480a97a5 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -122,9 +122,9 @@ BOOST_AUTO_TEST_CASE(mixed_action_call_tags_test) { try { // Make sure we can push an action using `sum`. BOOST_REQUIRE_NO_THROW(t.push_action("callee"_n, "sum"_n, "callee"_n, mvo() - ("in1", 1) - ("in2", 2) - ("in3", 3))); + ("a", 1) + ("b", 2) + ("c", 3))); } FC_LOG_AND_RETHROW() } // Verify the receiver contract with only one sync call function works diff --git a/tests/unit/test_contracts/sync_call_callee.cpp b/tests/unit/test_contracts/sync_call_callee.cpp index 12720c1e78..863658bb33 100644 --- a/tests/unit/test_contracts/sync_call_callee.cpp +++ b/tests/unit/test_contracts/sync_call_callee.cpp @@ -17,6 +17,6 @@ void sync_call_callee::voidfunc() { } [[eosio::action, eosio::call]] -uint32_t sync_call_callee::sum(uint32_t in1, uint32_t in2, uint32_t in3) { - return in1 + in2 + in3; +uint32_t sync_call_callee::sum(uint32_t a, uint32_t b, uint32_t c) { + return a + b + c; } diff --git a/tests/unit/test_contracts/sync_call_callee.hpp b/tests/unit/test_contracts/sync_call_callee.hpp index 47a1d55d7d..220993bf04 100644 --- a/tests/unit/test_contracts/sync_call_callee.hpp +++ b/tests/unit/test_contracts/sync_call_callee.hpp @@ -15,7 +15,7 @@ class [[eosio::contract]] sync_call_callee : public eosio::contract{ void voidfunc(); [[eosio::action, eosio::call]] - uint32_t sum(uint32_t in1, uint32_t in2, uint32_t in3); + uint32_t sum(uint32_t a, uint32_t b, uint32_t c); using getten_func = eosio::call_wrapper<"getten"_n, &sync_call_callee::getten, uint32_t>; using getback_func = eosio::call_wrapper<"getback"_n, &sync_call_callee::getback, uint32_t>; From 67b646c7300277e584f41b20f9b3f4f18cab3f6f Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Mon, 12 May 2025 15:00:25 -0400 Subject: [PATCH 05/29] Add const back to members --- libraries/eosiolib/contracts/eosio/call.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index c4e1194243..bb217a111c 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -63,24 +63,24 @@ namespace eosio { /** * Name of the account the call is intended for */ - name receiver{}; + const name receiver{}; /** * indicating if the call is read only or not */ - execution_mode exec_mode = execution_mode::read_write; + const execution_mode exec_mode = execution_mode::read_write; /** * if the receiver contract does not have sync_call entry point or its signature * is invalid, when on_call_not_supported_mode is set to no_op, * the sync call is no op, otherwise the call is aborted and an exception is raised. */ - on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort; + const on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort; /** * Payload data */ - std::vector data{}; + const std::vector data{}; /** * Construct a new call object with receiver, name, and payload data From de050b2959b615b81d8fbd1d898fdb72a80d6576 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Tue, 13 May 2025 08:16:18 -0400 Subject: [PATCH 06/29] Automatically derive return type of sync call functions in wrapper --- libraries/eosiolib/contracts/eosio/call.hpp | 4 +++- libraries/eosiolib/contracts/eosio/detail.hpp | 15 +++++++++++++++ tests/unit/test_contracts/sync_call_callee.hpp | 6 +++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index bb217a111c..59296dfaa0 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -130,7 +130,7 @@ namespace eosio { * get(); * @endcode */ - template + template struct call_wrapper { template constexpr call_wrapper(Receiver&& receiver, execution_mode exec_mode = execution_mode::read_write, on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort) @@ -150,6 +150,8 @@ namespace eosio { return call(receiver, std::make_tuple(func_name, detail::deduced{std::forward(args)...}), exec_mode, not_supported_mode); } + using Return_Type = typename detail::function_traits::return_type; + template Return_Type operator()(Args&&... args)const { auto size = to_call(std::forward(args)...)(); diff --git a/libraries/eosiolib/contracts/eosio/detail.hpp b/libraries/eosiolib/contracts/eosio/detail.hpp index 3261ac8030..889bf496be 100644 --- a/libraries/eosiolib/contracts/eosio/detail.hpp +++ b/libraries/eosiolib/contracts/eosio/detail.hpp @@ -75,5 +75,20 @@ namespace eosio { namespace detail { return true; } + // For non-function-pointers (function_traits is undefined) + template + struct function_traits; + + // For non-const member function + template + struct function_traits { + using return_type = Ret; + }; + + // For const member function + template + struct function_traits { + using return_type = Ret; + }; /// @endcond }} // eosio detail diff --git a/tests/unit/test_contracts/sync_call_callee.hpp b/tests/unit/test_contracts/sync_call_callee.hpp index 220993bf04..c35d5ac520 100644 --- a/tests/unit/test_contracts/sync_call_callee.hpp +++ b/tests/unit/test_contracts/sync_call_callee.hpp @@ -17,8 +17,8 @@ class [[eosio::contract]] sync_call_callee : public eosio::contract{ [[eosio::action, eosio::call]] uint32_t sum(uint32_t a, uint32_t b, uint32_t c); - using getten_func = eosio::call_wrapper<"getten"_n, &sync_call_callee::getten, uint32_t>; - using getback_func = eosio::call_wrapper<"getback"_n, &sync_call_callee::getback, uint32_t>; + using getten_func = eosio::call_wrapper<"getten"_n, &sync_call_callee::getten>; + using getback_func = eosio::call_wrapper<"getback"_n, &sync_call_callee::getback>; using voidfunc_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc>; - using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum, uint32_t>; + using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum>; }; From 676233a28e21414d0ad680e6c3b2e8a8c1cd938c Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Tue, 13 May 2025 11:47:45 -0400 Subject: [PATCH 07/29] Use set_call_return_value from C linkage --- tools/include/eosio/codegen.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/include/eosio/codegen.hpp b/tools/include/eosio/codegen.hpp index 9518a37589..d2c3ad3f2c 100644 --- a/tools/include/eosio/codegen.hpp +++ b/tools/include/eosio/codegen.hpp @@ -275,7 +275,7 @@ namespace eosio { namespace cdt { call_function(); if (return_ty != "void") { ss << "const auto& packed_result = eosio::pack(result);\n"; - ss << "set_call_return_value((void*)packed_result.data(), packed_result.size());\n"; + ss << "::set_call_return_value((void*)packed_result.data(), packed_result.size());\n"; } ss << "}}\n"; } From b35845994b1d6f18be46304975e2adf0e5fcb1f5 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Tue, 13 May 2025 15:29:03 -0400 Subject: [PATCH 08/29] Add tests for updating and reading from tables using sync calls and read-only enforcement --- tests/integration/call_tests.cpp | 25 +++++++++++++ tests/integration/contracts.hpp.in | 4 ++ tests/unit/test_contracts/CMakeLists.txt | 2 + .../sync_call_addr_book_callee.cpp | 37 +++++++++++++++++++ .../sync_call_addr_book_callee.hpp | 32 ++++++++++++++++ .../sync_call_addr_book_caller.cpp | 30 +++++++++++++++ 6 files changed, 130 insertions(+) create mode 100644 tests/unit/test_contracts/sync_call_addr_book_callee.cpp create mode 100644 tests/unit/test_contracts/sync_call_addr_book_callee.hpp create mode 100644 tests/unit/test_contracts/sync_call_addr_book_caller.cpp diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index db480a97a5..7fbf37a752 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -171,4 +171,29 @@ BOOST_AUTO_TEST_CASE(unknown_function_test) { try { eosio_assert_code_is(8000000000000000003)); } FC_LOG_AND_RETHROW() } +// Verify adding/reading entries to/from a table, and read-only enforcement work +BOOST_AUTO_TEST_CASE(addr_book_tests) { try { + call_tester t({ + {"caller"_n, contracts::addr_book_caller_wasm(), contracts::addr_book_caller_abi().data()}, + {"callee"_n, contracts::addr_book_callee_wasm(), contracts::addr_book_callee_abi().data()} + }); + + // Try to add an entry using a read-only sync call + BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "upsertrdonly"_n, "caller"_n, mvo() + ("user", "alice") + ("first_name", "alice") + ("street", "123 Main St.")), + unaccessible_api, + fc_exception_message_contains("this API is not allowed in read only action/call")); + + // Add an entry using a read-write sync call + t.push_action("caller"_n, "upsert"_n, "caller"_n, mvo() + ("user", "alice") + ("first_name", "alice") + ("street", "123 Main St.")); + + // Read the inserted entry. "get"_n action will check the return value from the sync call + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "get"_n, "caller"_n, mvo() ("user", "alice"))); +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/integration/contracts.hpp.in b/tests/integration/contracts.hpp.in index c12ea15d7f..4ae6ef5bbf 100644 --- a/tests/integration/contracts.hpp.in +++ b/tests/integration/contracts.hpp.in @@ -48,5 +48,9 @@ namespace eosio::testing { static std::vector not_supported_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_not_supported.abi"); } static std::vector single_func_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_single_func.wasm"); } static std::vector single_func_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_single_func.abi"); } + static std::vector addr_book_callee_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_callee.wasm"); } + static std::vector addr_book_callee_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_callee.abi"); } + static std::vector addr_book_caller_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_caller.wasm"); } + static std::vector addr_book_caller_abi() { return read_abi("${CMAKE_BINARY_DIR}/../unit/test_contracts/sync_call_addr_book_caller.abi"); } }; } //ns eosio::testing diff --git a/tests/unit/test_contracts/CMakeLists.txt b/tests/unit/test_contracts/CMakeLists.txt index 9e33779b7c..f94d236015 100644 --- a/tests/unit/test_contracts/CMakeLists.txt +++ b/tests/unit/test_contracts/CMakeLists.txt @@ -17,6 +17,8 @@ add_contract(sync_call_caller sync_call_caller sync_call_caller.cpp) add_contract(sync_call_callee sync_call_callee sync_call_callee.cpp) add_contract(sync_call_not_supported sync_call_not_supported sync_call_not_supported.cpp) add_contract(sync_call_single_func sync_call_single_func sync_call_single_func.cpp) +add_contract(sync_call_addr_book_callee sync_call_addr_book_callee sync_call_addr_book_callee.cpp) +add_contract(sync_call_addr_book_caller sync_call_addr_book_caller sync_call_addr_book_caller.cpp) add_contract(capi_tests capi_tests capi/capi.c capi/action.c capi/chain.c capi/crypto.c capi/db.c capi/permission.c capi/print.c capi/privileged.c capi/system.c capi/transaction.c capi/call.c) diff --git a/tests/unit/test_contracts/sync_call_addr_book_callee.cpp b/tests/unit/test_contracts/sync_call_addr_book_callee.cpp new file mode 100644 index 0000000000..347c0f23fd --- /dev/null +++ b/tests/unit/test_contracts/sync_call_addr_book_callee.cpp @@ -0,0 +1,37 @@ +#include "sync_call_addr_book_callee.hpp" +#include + +using namespace eosio; + +void sync_call_addr_book_callee::upsert(name user, std::string first_name, std::string street) { + // Intentionally leave out require_auth(user) to test upsert cannot be called as a read_only + // sync call + + address_index addresses(get_first_receiver(), get_first_receiver().value); + auto iterator = addresses.find(user.value); + if( iterator == addresses.end() ) + { + addresses.emplace(user, [&]( auto& row ) { + row.key = user; + row.first_name = first_name; + row.street = street; + }); + } + else { + addresses.modify(iterator, user, [&]( auto& row ) { + row.key = user; + row.first_name = first_name; + row.street = street; + }); + } +} + +person_info sync_call_addr_book_callee::get(name user) { + address_index addresses(get_first_receiver(), get_first_receiver().value); + + auto iterator = addresses.find(user.value); + check(iterator != addresses.end(), "Record does not exist"); + + return person_info{ .first_name = iterator->first_name, + .street = iterator->street }; +} diff --git a/tests/unit/test_contracts/sync_call_addr_book_callee.hpp b/tests/unit/test_contracts/sync_call_addr_book_callee.hpp new file mode 100644 index 0000000000..156d00e712 --- /dev/null +++ b/tests/unit/test_contracts/sync_call_addr_book_callee.hpp @@ -0,0 +1,32 @@ +#include +#include + +struct person_info { + std::string first_name; + std::string street; +}; + +class [[eosio::contract]] sync_call_addr_book_callee : public eosio::contract { +public: + sync_call_addr_book_callee(eosio::name receiver, eosio::name code, eosio::datastream ds): contract(receiver, code, ds) {} + + [[eosio::call]] + void upsert(eosio::name user, std::string first_name, std::string street); + + [[eosio::call]] + person_info get(eosio::name user); + + using upsert_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert>; + using get_func = eosio::call_wrapper<"get"_n, &sync_call_addr_book_callee::get>; + +private: + struct [[eosio::table]] person { + eosio::name key; + std::string first_name; + std::string street; + + uint64_t primary_key() const { return key.value; } + }; + + using address_index = eosio::multi_index<"people"_n, person>; +}; diff --git a/tests/unit/test_contracts/sync_call_addr_book_caller.cpp b/tests/unit/test_contracts/sync_call_addr_book_caller.cpp new file mode 100644 index 0000000000..86884f01de --- /dev/null +++ b/tests/unit/test_contracts/sync_call_addr_book_caller.cpp @@ -0,0 +1,30 @@ +#include "sync_call_addr_book_callee.hpp" + +#include +#include + +class [[eosio::contract]] sync_call_addr_book_caller : public eosio::contract{ +public: + using contract::contract; + + // Insert an entry using read_only, which will fail + [[eosio::action]] + void upsertrdonly(eosio::name user, std::string first_name, std::string street) { + sync_call_addr_book_callee::upsert_func{"callee"_n, eosio::execution_mode::read_only}(user, first_name, street); + } + + // Insert an entry + [[eosio::action]] + void upsert(eosio::name user, std::string first_name, std::string street) { + sync_call_addr_book_callee::upsert_func{"callee"_n}(user, first_name, street); + } + + // Read an entry + [[eosio::action]] + person_info get(eosio::name user) { + auto user_info = sync_call_addr_book_callee::get_func{"callee"_n}(user); + eosio::check(user_info.first_name == "alice", "first name not alice"); + eosio::check(user_info.street == "123 Main St.", "street not 123 Main St."); + return user_info; + } +}; From 755ddc5d5450dcc98f38a65b0aa5aab8f983e10e Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Wed, 14 May 2025 18:24:07 -0400 Subject: [PATCH 09/29] Refactor call_warpper - Remove unnecessary call class - Make C++ call host function simply invoke C version - Make call_wrapper constructor take only one argument (receiver); all others are template parameters - Make call_wrapper handle high level logic --- libraries/eosiolib/contracts/eosio/call.hpp | 116 +++++------------- tests/integration/call_tests.cpp | 17 ++- .../sync_call_addr_book_callee.hpp | 1 + .../sync_call_addr_book_caller.cpp | 2 +- .../unit/test_contracts/sync_call_caller.cpp | 15 ++- .../sync_call_not_supported.cpp | 36 +++--- 6 files changed, 69 insertions(+), 118 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 59296dfaa0..83e912ee2e 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -35,6 +35,10 @@ namespace eosio { * @note There are some methods from the @ref call that can be used directly from C++ */ + inline int64_t call(uint64_t receiver, uint64_t flags, const char* data, size_t data_size) { + return internal_use_do_not_use::call(receiver, flags, data, data_size); + } + inline uint32_t get_call_return_value( void* mem, uint32_t len ) { return internal_use_do_not_use::get_call_return_value(mem, len); } @@ -55,68 +59,7 @@ namespace eosio { enum on_call_not_supported_mode { abort = 0, no_op = 1 }; /** - * This is the packed representation of a call - * - * @ingroup call - */ - struct call { - /** - * Name of the account the call is intended for - */ - const name receiver{}; - - /** - * indicating if the call is read only or not - */ - const execution_mode exec_mode = execution_mode::read_write; - - /** - * if the receiver contract does not have sync_call entry point or its signature - * is invalid, when on_call_not_supported_mode is set to no_op, - * the sync call is no op, otherwise the call is aborted and an exception is raised. - */ - const on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort; - - /** - * Payload data - */ - const std::vector data{}; - - /** - * Construct a new call object with receiver, name, and payload data - * - * @tparam T - Type of call data, must be serializable by `pack(...)` - * @param receiver - The name of the account this call is intended for - * @param flags - The flags - * @param payload - The call data that will be serialized via pack into data - */ - template - call( struct name receiver, T&& payload, execution_mode exec_mode = execution_mode::read_write, on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort) - : receiver(receiver) - , exec_mode(exec_mode) - , not_supported_mode(not_supported_mode) - , data(pack(std::forward(payload))) {} - - /// @cond INTERNAL - EOSLIB_SERIALIZE( call, (receiver)(exec_mode)(not_supported_mode)(data) ) - /// @endcond - - /** - * Make a call using the functor operator - */ - int64_t operator()() const { - uint64_t flags = (exec_mode == execution_mode::read_only) ? 0x01 : 0x00; // last bit indicating read only - auto retval = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); - - if (retval == -1) { // sync call is not supported by the receiver contract - check(not_supported_mode == on_call_not_supported_mode::no_op, "receiver does not support sync call but on_call_not_supported_mode is set to abort"); - } - return retval; - } - }; - - /** - * Wrapper for a call object. + * Wrapper for simplifying making a sync call * * @brief Used to wrap an a particular sync call to simplify the process of other contracts making sync calls to the "wrapped" call. * Example: @@ -130,41 +73,50 @@ namespace eosio { * get(); * @endcode */ - template + template struct call_wrapper { template - constexpr call_wrapper(Receiver&& receiver, execution_mode exec_mode = execution_mode::read_write, on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort) + constexpr call_wrapper(Receiver&& receiver) : receiver(std::forward(receiver)) - , exec_mode(exec_mode) - , not_supported_mode(not_supported_mode) {} static constexpr eosio::name func_name = eosio::name(Func_Name); eosio::name receiver {}; - execution_mode exec_mode = execution_mode::read_write; - on_call_not_supported_mode not_supported_mode = on_call_not_supported_mode::abort; - - template - call to_call(Args&&... args)const { - static_assert(detail::type_check()); - return call(receiver, std::make_tuple(func_name, detail::deduced{std::forward(args)...}), exec_mode, not_supported_mode); - } - using Return_Type = typename detail::function_traits::return_type; + using ret_type = typename detail::function_traits::return_type; template - Return_Type operator()(Args&&... args)const { - auto size = to_call(std::forward(args)...)(); + ret_type operator()(Args&&... args)const { + uint64_t flags = 0x00; + if constexpr (Exec_Mode == execution_mode::read_only) { + flags = 0x01; + } + const std::vector data{ pack(std::make_tuple(func_name, detail::deduced{std::forward(args)...})) }; + + auto ret_val_size = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); + + if (ret_val_size < 0) { + if constexpr (Not_Supported_Mode == on_call_not_supported_mode::abort) { + check(false, "receiver does not support sync call while on_call_not_supported_mode is set to abort"); + } else { + if constexpr (std::is_void::value) { + return; + } else if constexpr (std::is_default_constructible_v) { + return {}; + } else { + static_assert(std::is_default_constructible_v, "Return type of on_call_not_supported_mode::no_op function must be default constructible"); + } + } + } - if constexpr (std::is_void::value) { + if constexpr (std::is_void::value) { return; } else { constexpr size_t max_stack_buffer_size = 512; - char* buffer = (char*)(max_stack_buffer_size < size ? malloc(size) : alloca(size)); - internal_use_do_not_use::get_call_return_value(buffer, size); - return unpack(buffer, size); + char* buffer = (char*)(max_stack_buffer_size < ret_val_size ? malloc(ret_val_size) : alloca(ret_val_size)); // intentionally no `free()` is called. the memory will be reset after execution + internal_use_do_not_use::get_call_return_value(buffer, ret_val_size); + return unpack(buffer, ret_val_size); } } - }; } // namespace eosio diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index 7fbf37a752..3b45956cba 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -146,17 +146,14 @@ BOOST_AUTO_TEST_CASE(sync_call_not_supported_test) { try { {"caller"_n, contracts::not_supported_wasm(), contracts::not_supported_abi().data()} }); - // * sync_call_not_supported contract only has actions - // * no_op_if_receiver_not_support_sync_call is set - // so the call is just a no-op - BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "noopset"_n, "caller"_n, {})); - - // * sync_call_not_supported contract only has actions - // * no_op_if_receiver_not_support_sync_call is NOT set - // so the call aborts - BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "noopnotset"_n, "caller"_n, {}), + // sync_call_not_supported contract only has actions and on_call_not_supported_mode + // is passed in as no-op, so the call is just a no-op + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "noopifnot"_n, "caller"_n, {})); + + // on_call_not_supported_mode is passed in as abort, so the call aborts + BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "abortifnot"_n, "caller"_n, {}), eosio_assert_message_exception, - fc_exception_message_contains("receiver does not support sync call but on_call_not_supported_mode is set to abort")); + fc_exception_message_contains("receiver does not support sync call while on_call_not_supported_mode is set to abort")); } FC_LOG_AND_RETHROW() } // Verify calling an unknown function will result in an eosio_assert diff --git a/tests/unit/test_contracts/sync_call_addr_book_callee.hpp b/tests/unit/test_contracts/sync_call_addr_book_callee.hpp index 156d00e712..a06f08c075 100644 --- a/tests/unit/test_contracts/sync_call_addr_book_callee.hpp +++ b/tests/unit/test_contracts/sync_call_addr_book_callee.hpp @@ -16,6 +16,7 @@ class [[eosio::contract]] sync_call_addr_book_callee : public eosio::contract { [[eosio::call]] person_info get(eosio::name user); + using upsert_read_only_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert, eosio::execution_mode::read_only>; using upsert_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert>; using get_func = eosio::call_wrapper<"get"_n, &sync_call_addr_book_callee::get>; diff --git a/tests/unit/test_contracts/sync_call_addr_book_caller.cpp b/tests/unit/test_contracts/sync_call_addr_book_caller.cpp index 86884f01de..bd65cfa02e 100644 --- a/tests/unit/test_contracts/sync_call_addr_book_caller.cpp +++ b/tests/unit/test_contracts/sync_call_addr_book_caller.cpp @@ -10,7 +10,7 @@ class [[eosio::contract]] sync_call_addr_book_caller : public eosio::contract{ // Insert an entry using read_only, which will fail [[eosio::action]] void upsertrdonly(eosio::name user, std::string first_name, std::string street) { - sync_call_addr_book_callee::upsert_func{"callee"_n, eosio::execution_mode::read_only}(user, first_name, street); + sync_call_addr_book_callee::upsert_read_only_func{"callee"_n}(user, first_name, street); } // Insert an entry diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index df076b12d1..4441c818bf 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -10,7 +10,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ // Using host function directly [[eosio::action]] void hstretvaltst() { - auto expected_size = eosio::call("callee"_n, "getten"_n)(); + const std::vector data{ eosio::pack("getten"_n.value) }; + auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); std::vector return_value; @@ -31,7 +32,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ [[eosio::action]] void hstoneprmtst() { // `getback(uint32_t p)` returns p - auto expected_size = eosio::call("callee"_n, std::make_tuple("getback"_n, 5))(); + const std::vector data{ eosio::pack(std::make_tuple("getback"_n, 5)) }; + auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); std::vector return_value; @@ -51,7 +53,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ // Using host function directly, testing multiple parameters passing [[eosio::action]] void hstmulprmtst() { - auto expected_size = eosio::call("callee"_n, std::make_tuple("sum"_n, 10, 20, 30))(); + const std::vector data{ eosio::pack(std::make_tuple("sum"_n, 10, 20, 30)) }; + auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); std::vector return_value; @@ -70,7 +73,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ [[eosio::action]] void hstvodfuntst() { - auto expected_size = eosio::call("callee"_n, "voidfunc"_n)(); + const std::vector data{ eosio::pack("voidfunc"_n.value) }; + auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size == 0, "call did not return 0"); // void function. return value size should be 0 } @@ -82,6 +86,7 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ [[eosio::action]] void unknwnfuntst() { - eosio::call("callee"_n, "unknwnfunc"_n)(); // unknwnfunc will never be in "callee"_n contract + const std::vector data{ eosio::pack("unknwnfunc"_n.value) }; // unknwnfunc is not in "callee"_n contract + auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); } }; diff --git a/tests/unit/test_contracts/sync_call_not_supported.cpp b/tests/unit/test_contracts/sync_call_not_supported.cpp index e210160a5b..e974a37929 100644 --- a/tests/unit/test_contracts/sync_call_not_supported.cpp +++ b/tests/unit/test_contracts/sync_call_not_supported.cpp @@ -1,34 +1,30 @@ #include #include +// This contract does not tag any function by `call` attribute. +// No `sync_call` entry point is generated. +// Sync calls to any functions in this contract will fail. class [[eosio::contract]] sync_call_not_supported : public eosio::contract{ public: using contract::contract; - // * sync call is not supported as no method is taged by `call` - // * no_op_if_receiver_no_support_sync_call is set [[eosio::action]] - void noopset() { - std::vector data{}; - - // For now, because sync_call entry point has not been implemented yet and - // on_call_not_supported_mode is set no_op, call should return -1 - auto rc = eosio::call("caller"_n, data, eosio::execution_mode::read_write, eosio::on_call_not_supported_mode::no_op)(); - eosio::check(rc == -1, "call did not return -1"); - - // call was not executed. return value size should be 0 - std::vector value(10); - auto size = eosio::get_call_return_value(value.data(), value.size()); - eosio::check(size == 0, "return value size is not 0"); + void dummy() { } + using dummy_func = eosio::call_wrapper<"dummy"_n, &sync_call_not_supported::dummy>; + using dummy_no_op_func = eosio::call_wrapper<"dummy"_n, &sync_call_not_supported::dummy, eosio::execution_mode::read_write, eosio::on_call_not_supported_mode::no_op>; - // sync call not supported, no_op_if_receiver_no_support_sync_call not set + // request no-op [[eosio::action]] - void noopnotset() { - std::vector data{}; + void noopifnot() { + dummy_no_op_func dummy_f{"caller"_n}; + dummy_f(); + } - // For now, because sync_call entry point has not been implemented yet and - // no_op_if_receiver_no_support_sync_call is not set, call should fail - eosio::call("caller"_n, data)(); + // request abort + [[eosio::action]] + void abortifnot() { + dummy_func dummy_f{"caller"_n}; // default is abort + dummy_f(); } }; From e7982465862cece509f1115babd02a9cb3afb4ef Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 16 May 2025 18:45:21 -0400 Subject: [PATCH 10/29] Implement data header and validate it --- libraries/eosiolib/contracts/eosio/call.hpp | 15 +++++++-- tests/integration/call_tests.cpp | 6 ++-- .../unit/test_contracts/sync_call_caller.cpp | 29 +++++++++++++---- tools/include/eosio/codegen.hpp | 32 +++++++++++-------- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 83e912ee2e..8727efb920 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -58,6 +58,13 @@ namespace eosio { // Default is abort enum on_call_not_supported_mode { abort = 0, no_op = 1 }; + struct call_data_header { + uint32_t version = 0; + uint64_t func_name = 0; + + EOSLIB_SERIALIZE(call_data_header, (version)(func_name)) + }; + /** * Wrapper for simplifying making a sync call * @@ -80,7 +87,7 @@ namespace eosio { : receiver(std::forward(receiver)) {} - static constexpr eosio::name func_name = eosio::name(Func_Name); + static constexpr eosio::name function_name = eosio::name(Func_Name); eosio::name receiver {}; using ret_type = typename detail::function_traits::return_type; @@ -91,7 +98,11 @@ namespace eosio { if constexpr (Exec_Mode == execution_mode::read_only) { flags = 0x01; } - const std::vector data{ pack(std::make_tuple(func_name, detail::deduced{std::forward(args)...})) }; + + call_data_header header{ .version = 0, + .func_name = function_name.value }; + + const std::vector data{ pack(std::make_tuple(header, detail::deduced{std::forward(args)...})) }; auto ret_val_size = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index 3b45956cba..93ba758449 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -156,16 +156,14 @@ BOOST_AUTO_TEST_CASE(sync_call_not_supported_test) { try { fc_exception_message_contains("receiver does not support sync call while on_call_not_supported_mode is set to abort")); } FC_LOG_AND_RETHROW() } -// Verify calling an unknown function will result in an eosio_assert +// Verify header validation BOOST_AUTO_TEST_CASE(unknown_function_test) { try { call_tester t({ {"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()}, {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} }); - BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "unknwnfuntst"_n, "caller"_n, {}), - eosio_assert_code_exception, - eosio_assert_code_is(8000000000000000003)); + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hdrvaltest"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } // Verify adding/reading entries to/from a table, and read-only enforcement work diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index 4441c818bf..58bb9b39cc 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -3,6 +3,8 @@ #include #include +using namespace eosio; + class [[eosio::contract]] sync_call_caller : public eosio::contract{ public: using contract::contract; @@ -10,7 +12,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ // Using host function directly [[eosio::action]] void hstretvaltst() { - const std::vector data{ eosio::pack("getten"_n.value) }; + call_data_header header{ .version = 0, .func_name = "getten"_n.value }; + const std::vector data{ eosio::pack(header) }; auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); @@ -32,7 +35,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ [[eosio::action]] void hstoneprmtst() { // `getback(uint32_t p)` returns p - const std::vector data{ eosio::pack(std::make_tuple("getback"_n, 5)) }; + call_data_header header{ .version = 0, .func_name = "getback"_n.value }; + const std::vector data{ eosio::pack(std::make_tuple(header, 5)) }; auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); @@ -53,7 +57,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ // Using host function directly, testing multiple parameters passing [[eosio::action]] void hstmulprmtst() { - const std::vector data{ eosio::pack(std::make_tuple("sum"_n, 10, 20, 30)) }; + call_data_header header{ .version = 0, .func_name = "sum"_n.value }; + const std::vector data{ eosio::pack(std::make_tuple(header, 10, 20, 30)) }; auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); @@ -73,7 +78,8 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ [[eosio::action]] void hstvodfuntst() { - const std::vector data{ eosio::pack("voidfunc"_n.value) }; + call_data_header header{ .version = 0, .func_name = "voidfunc"_n.value }; + const std::vector data{ eosio::pack(header) }; auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); eosio::check(expected_size == 0, "call did not return 0"); // void function. return value size should be 0 } @@ -85,8 +91,17 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ } [[eosio::action]] - void unknwnfuntst() { - const std::vector data{ eosio::pack("unknwnfunc"_n.value) }; // unknwnfunc is not in "callee"_n contract - auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); + void hdrvaltest() { + // Verify function name validation works + call_data_header unkwn_func_header{ .version = 0, .func_name = "unknwnfunc"_n.value }; + const std::vector unkwn_func_data{ eosio::pack(unkwn_func_header) }; // unknwnfunc is not in "callee"_n contract + auto status = eosio::call("callee"_n.value, 0, unkwn_func_data.data(), unkwn_func_data.size()); + eosio::check(status == -3, "call did not return -3 for unknown function"); + + // Verify version validation works + call_data_header bad_version_header{ .version = 1, .func_name = "sum"_n.value }; // version 1 is not supported + const std::vector bad_version_data{ eosio::pack(bad_version_header) }; + status = eosio::call("callee"_n.value, 0, bad_version_data.data(), bad_version_data.size()); + eosio::check(status == -2, "call did not return -2 for invalid version"); } }; diff --git a/tools/include/eosio/codegen.hpp b/tools/include/eosio/codegen.hpp index d2c3ad3f2c..77e8afe5cd 100644 --- a/tools/include/eosio/codegen.hpp +++ b/tools/include/eosio/codegen.hpp @@ -232,7 +232,7 @@ namespace eosio { namespace cdt { std::string nm = decl->getNameAsString()+"_"+decl->getParent()->getNameAsString(); if (cg.is_eosio_contract(decl, cg.contract_name)) { ss << "\n\n#include \n"; - ss << "#include \n"; + ss << "#include \n"; ss << "extern \"C\" {\n"; const auto& return_ty = decl->getReturnType().getAsString(); if (return_ty != "void") { @@ -245,7 +245,7 @@ namespace eosio { namespace cdt { ss << func_name << nm; ss << "\"))) void " << func_name << nm << "(unsigned long long sender, unsigned long long receiver, size_t data_size, void* data) {\n"; ss << "eosio::datastream ds{(char*)data, data_size};\n"; - ss << "unsigned long long func_name; ds >> func_name;\n"; // skip called function name + ss << "eosio::call_data_header header; ds >> header;\n"; // skip header int i=0; for (auto param : decl->parameters()) { clang::LangOptions lang_opts; @@ -281,26 +281,32 @@ namespace eosio { namespace cdt { } } - // Generate get_sync_call_func_name which returns called function name - static void create_get_sync_call_func_name(std::stringstream& ss) { + // Generate get_sync_call_data_version which returns the version of call data. + // In version 0, call data is packed as header + arguments, where + // header is `struct header { uint32_t version; uint64_t func_name }` + static void create_get_sync_call_data_header(std::stringstream& ss) { ss << "\n\n#include \n"; - ss << "#include \n"; + ss << "#include \n"; ss << "extern \"C\" {\n"; - ss << "__attribute__((weak)) unsigned long long __eos_get_sync_call_func_name_(void* data) {\n"; - ss << "eosio::datastream ds{(char*)data, sizeof(unsigned long long)};\n"; - ss << "unsigned long long func_name; ds >> func_name;\n"; - ss << "return func_name;\n"; + ss << "__attribute__((weak)) void* __eos_get_sync_call_data_header_(void* data) {\n"; + ss << "size_t size = sizeof(eosio::call_data_header);\n"; + ss << "eosio::datastream ds{(char*)data, size};\n"; + ss << "eosio::call_data_header header; ds >> header;\n"; + ss << "void* ptr = malloc(size);\n"; + ss << "memcpy(ptr, &header, size);\n"; + ss << "return ptr;\n"; ss << "}}\n"; } - // Generate get_sync_call_data which returns call data + // Generate get_sync_call_data which returns call data which consists of + // header and arguments static void create_get_sync_call_data(std::stringstream& ss) { ss << "\n\n#include \n"; ss << "#include \n"; ss << "extern \"C\" {\n"; ss << "__attribute__((eosio_wasm_import)) uint32_t get_call_data(void*, uint32_t);\n"; ss << "__attribute__((weak)) void* __eos_get_sync_call_data_(unsigned long size) {\n"; - ss << "void* data = malloc(size);\n"; + ss << "void* data = malloc(size);\n"; // store data in linear memory ss << "::get_call_data(data, size);\n"; ss << "return data;\n"; ss << "}}\n"; @@ -366,10 +372,10 @@ namespace eosio { namespace cdt { CDT_ERROR("codegen_error", decl->getLocation(), std::string("call name (")+s+") is not a valid eosio name"); }); - // Genereate create_get_sync_call_data and get_sync_call_func_name only once + // Genereate create_get_sync_call_data and create_get_sync_call_data_header only once if (_call_set.empty()) { create_get_sync_call_data(ss); - create_get_sync_call_func_name(ss); + create_get_sync_call_data_header(ss); } if (!_call_set.count(name)) From ca6f0a1aee2c46e98a3643e48513b4c97a66901a Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 16 May 2025 19:35:45 -0400 Subject: [PATCH 11/29] Bump llvm submodule to call_wrapper --- cdt-llvm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdt-llvm b/cdt-llvm index 859a288611..6720393c73 160000 --- a/cdt-llvm +++ b/cdt-llvm @@ -1 +1 @@ -Subproject commit 859a2886116260b0ed5c4b94cfc2032d8b743f9e +Subproject commit 6720393c738a2bf544ca0276bf49cb78a9361928 From 7cab68c5686a48a61c57e8bbcc472f84f9c2bf36 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 16 May 2025 19:42:17 -0400 Subject: [PATCH 12/29] Bump cdt-llvm --- cdt-llvm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdt-llvm b/cdt-llvm index 6720393c73..1a94400c77 160000 --- a/cdt-llvm +++ b/cdt-llvm @@ -1 +1 @@ -Subproject commit 6720393c738a2bf544ca0276bf49cb78a9361928 +Subproject commit 1a94400c7765abd88dad5563151edf3655091888 From de416fd0e83cf7c3c466b8f255ad1cfc0c4a7bcc Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 16 May 2025 19:45:27 -0400 Subject: [PATCH 13/29] Point antelope-spring-dev to return_status branch temporarily --- .cicd/defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cicd/defaults.json b/.cicd/defaults.json index 8f75428a37..f35219f5c8 100644 --- a/.cicd/defaults.json +++ b/.cicd/defaults.json @@ -1,6 +1,6 @@ { "antelope-spring-dev":{ - "target":"sync_call", + "target":"return_status", "prerelease":false } } From 79d466e089bebef896942707a76248b6bfb28f31 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Sat, 17 May 2025 13:29:54 -0400 Subject: [PATCH 14/29] Add type checks for arguments --- libraries/eosiolib/contracts/eosio/call.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 8727efb920..8b9d06fae1 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -94,6 +94,8 @@ namespace eosio { template ret_type operator()(Args&&... args)const { + static_assert(detail::type_check()); + uint64_t flags = 0x00; if constexpr (Exec_Mode == execution_mode::read_only) { flags = 0x01; From 4848fb242315f3a570b83ca28c2134f00634e743 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Tue, 20 May 2025 08:17:53 -0400 Subject: [PATCH 15/29] Rename execution_mode to access_mode, and on_call_not_supported_mode to support_mode for simplicity --- libraries/eosiolib/contracts/eosio/call.hpp | 18 +++++++++--------- tests/integration/call_tests.cpp | 2 +- .../sync_call_addr_book_callee.hpp | 2 +- .../test_contracts/sync_call_not_supported.cpp | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 8b9d06fae1..5062c82504 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -51,12 +51,12 @@ namespace eosio { internal_use_do_not_use::set_call_return_value(mem, len); } - // Request a sync call is read_write or read_only. Default is read_write - enum execution_mode { read_write = 0, read_only = 1 }; + // Indicate whether a sync call is read_write or read_only. Default is read_write + enum access_mode { read_write = 0, read_only = 1 }; - // Behaviour of a sync call if the receiver does not support sync calls + // Indicate the action to take if the receiver does not support sync calls. // Default is abort - enum on_call_not_supported_mode { abort = 0, no_op = 1 }; + enum support_mode { abort = 0, no_op = 1 }; struct call_data_header { uint32_t version = 0; @@ -80,7 +80,7 @@ namespace eosio { * get(); * @endcode */ - template + template struct call_wrapper { template constexpr call_wrapper(Receiver&& receiver) @@ -97,7 +97,7 @@ namespace eosio { static_assert(detail::type_check()); uint64_t flags = 0x00; - if constexpr (Exec_Mode == execution_mode::read_only) { + if constexpr (Access_Mode == access_mode::read_only) { flags = 0x01; } @@ -109,15 +109,15 @@ namespace eosio { auto ret_val_size = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); if (ret_val_size < 0) { - if constexpr (Not_Supported_Mode == on_call_not_supported_mode::abort) { - check(false, "receiver does not support sync call while on_call_not_supported_mode is set to abort"); + if constexpr (Support_Mode == support_mode::abort) { + check(false, "receiver does not support sync call but support_mode is set to abort"); } else { if constexpr (std::is_void::value) { return; } else if constexpr (std::is_default_constructible_v) { return {}; } else { - static_assert(std::is_default_constructible_v, "Return type of on_call_not_supported_mode::no_op function must be default constructible"); + static_assert(std::is_default_constructible_v, "Return type of support_mode::no_op function must be default constructible"); } } } diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index 93ba758449..94bcbbda2c 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -153,7 +153,7 @@ BOOST_AUTO_TEST_CASE(sync_call_not_supported_test) { try { // on_call_not_supported_mode is passed in as abort, so the call aborts BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "abortifnot"_n, "caller"_n, {}), eosio_assert_message_exception, - fc_exception_message_contains("receiver does not support sync call while on_call_not_supported_mode is set to abort")); + fc_exception_message_contains("receiver does not support sync call but support_mode is set to abort")); } FC_LOG_AND_RETHROW() } // Verify header validation diff --git a/tests/unit/test_contracts/sync_call_addr_book_callee.hpp b/tests/unit/test_contracts/sync_call_addr_book_callee.hpp index a06f08c075..2cf0dc0bc2 100644 --- a/tests/unit/test_contracts/sync_call_addr_book_callee.hpp +++ b/tests/unit/test_contracts/sync_call_addr_book_callee.hpp @@ -16,7 +16,7 @@ class [[eosio::contract]] sync_call_addr_book_callee : public eosio::contract { [[eosio::call]] person_info get(eosio::name user); - using upsert_read_only_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert, eosio::execution_mode::read_only>; + using upsert_read_only_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert, eosio::access_mode::read_only>; using upsert_func = eosio::call_wrapper<"upsert"_n, &sync_call_addr_book_callee::upsert>; using get_func = eosio::call_wrapper<"get"_n, &sync_call_addr_book_callee::get>; diff --git a/tests/unit/test_contracts/sync_call_not_supported.cpp b/tests/unit/test_contracts/sync_call_not_supported.cpp index e974a37929..0688ae4330 100644 --- a/tests/unit/test_contracts/sync_call_not_supported.cpp +++ b/tests/unit/test_contracts/sync_call_not_supported.cpp @@ -12,7 +12,7 @@ class [[eosio::contract]] sync_call_not_supported : public eosio::contract{ void dummy() { } using dummy_func = eosio::call_wrapper<"dummy"_n, &sync_call_not_supported::dummy>; - using dummy_no_op_func = eosio::call_wrapper<"dummy"_n, &sync_call_not_supported::dummy, eosio::execution_mode::read_write, eosio::on_call_not_supported_mode::no_op>; + using dummy_no_op_func = eosio::call_wrapper<"dummy"_n, &sync_call_not_supported::dummy, eosio::access_mode::read_write, eosio::support_mode::no_op>; // request no-op [[eosio::action]] From 8af48755a07eaef821888ebb975fb8230140b1b0 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Tue, 20 May 2025 14:20:48 -0400 Subject: [PATCH 16/29] Return std::optional for support_mode::no_op calls; add comprehensive tests --- libraries/eosiolib/contracts/eosio/call.hpp | 52 +++++++++++++------ tests/integration/call_tests.cpp | 38 +++++++++++--- .../unit/test_contracts/sync_call_callee.hpp | 3 ++ .../unit/test_contracts/sync_call_caller.cpp | 43 +++++++++++++++ .../sync_call_not_supported.cpp | 36 +++---------- .../sync_call_not_supported.hpp | 23 ++++++++ 6 files changed, 144 insertions(+), 51 deletions(-) create mode 100644 tests/unit/test_contracts/sync_call_not_supported.hpp diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 5062c82504..bb7b6aa2d9 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -58,6 +58,13 @@ namespace eosio { // Default is abort enum support_mode { abort = 0, no_op = 1 }; + // For a void function, when support_mode is set to no_op, the call_wrapper. + // returns `std::optional`. If the optional has no value, it indicates + // the call was op-op; if the optional has a value of `void_call`, the call + // executed successfully. + struct void_call { + }; + struct call_data_header { uint32_t version = 0; uint64_t func_name = 0; @@ -90,10 +97,20 @@ namespace eosio { static constexpr eosio::name function_name = eosio::name(Func_Name); eosio::name receiver {}; - using ret_type = typename detail::function_traits::return_type; + using orig_ret_type = typename detail::function_traits::return_type; + + using return_type = std::conditional_t< + Support_Mode == support_mode::abort, // if Support_Mode is abort + orig_ret_type, // use the original return type + std::conditional_t< // else + std::is_void::value, // original return type is void + std::optional, // use optional of empty struct + std::optional // use optional of original return type + > + >; template - ret_type operator()(Args&&... args)const { + return_type operator()(Args&&... args)const { static_assert(detail::type_check()); uint64_t flags = 0x00; @@ -108,27 +125,32 @@ namespace eosio { auto ret_val_size = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); - if (ret_val_size < 0) { + if (ret_val_size < 0) { // the receiver does not support sync calls if constexpr (Support_Mode == support_mode::abort) { check(false, "receiver does not support sync call but support_mode is set to abort"); } else { - if constexpr (std::is_void::value) { - return; - } else if constexpr (std::is_default_constructible_v) { - return {}; - } else { - static_assert(std::is_default_constructible_v, "Return type of support_mode::no_op function must be default constructible"); - } + return std::nullopt; } } - if constexpr (std::is_void::value) { + // The sync call has been executed by the receiver + if constexpr (std::is_void::value) { return; } else { - constexpr size_t max_stack_buffer_size = 512; - char* buffer = (char*)(max_stack_buffer_size < ret_val_size ? malloc(ret_val_size) : alloca(ret_val_size)); // intentionally no `free()` is called. the memory will be reset after execution - internal_use_do_not_use::get_call_return_value(buffer, ret_val_size); - return unpack(buffer, ret_val_size); + if constexpr (Support_Mode == support_mode::no_op && std::is_void::value) { + return void_call{}; + } else { + constexpr size_t max_stack_buffer_size = 512; + char* buffer = (char*)(max_stack_buffer_size < ret_val_size ? malloc(ret_val_size) : alloca(ret_val_size)); // intentionally no `free()` is called. the memory will be reset after execution + internal_use_do_not_use::get_call_return_value(buffer, ret_val_size); + auto ret_val = unpack(buffer, ret_val_size); + + if constexpr (Support_Mode == support_mode::no_op) { + return std::make_optional(ret_val); + } else { + return ret_val; + } + } } } }; diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index 94bcbbda2c..62f9c1bf1d 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -140,20 +140,42 @@ BOOST_AUTO_TEST_CASE(single_function_test) { try { BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "hstretvaltst"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } -// Verify no_op_if_receiver_not_support_sync_call flag works -BOOST_AUTO_TEST_CASE(sync_call_not_supported_test) { try { +// Verify support_mode for void and non-void sync calls if calls are a failure +BOOST_AUTO_TEST_CASE(sync_call_support_mode_failure_test) { try { call_tester t({ - {"caller"_n, contracts::not_supported_wasm(), contracts::not_supported_abi().data()} + {"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()}, + {"callee"_n, contracts::not_supported_wasm(), contracts::not_supported_abi().data()} }); - // sync_call_not_supported contract only has actions and on_call_not_supported_mode - // is passed in as no-op, so the call is just a no-op - BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "noopifnot"_n, "caller"_n, {})); + // voidfncnoop uses support_mode::no_op + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "voidfncnoop"_n, "caller"_n, {})); - // on_call_not_supported_mode is passed in as abort, so the call aborts - BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "abortifnot"_n, "caller"_n, {}), + // voidfncabort uses default support_mode::abort + BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "voidfncabort"_n, "caller"_n, {}), eosio_assert_message_exception, fc_exception_message_contains("receiver does not support sync call but support_mode is set to abort")); + + // intfuncnoop uses support_mode::no_op + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "intfuncnoop"_n, "caller"_n, {})); + + // intfuncabort uses default support_mode::abort + BOOST_CHECK_EXCEPTION(t.push_action("caller"_n, "intfuncabort"_n, "caller"_n, {}), + eosio_assert_message_exception, + fc_exception_message_contains("receiver does not support sync call but support_mode is set to abort")); +} FC_LOG_AND_RETHROW() } + +// Verify support_mode for void and non-void sync calls if call is successful +BOOST_AUTO_TEST_CASE(sync_call_support_mode_success_test) { try { + call_tester t({ + {"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()}, + {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} + }); + + // voidnoopsucc uses support_mode::no_op + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "voidnoopsucc"_n, "caller"_n, {})); + + // sumnoopsucc uses support_mode::no_op + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "sumnoopsucc"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } // Verify header validation diff --git a/tests/unit/test_contracts/sync_call_callee.hpp b/tests/unit/test_contracts/sync_call_callee.hpp index c35d5ac520..87b7a02f93 100644 --- a/tests/unit/test_contracts/sync_call_callee.hpp +++ b/tests/unit/test_contracts/sync_call_callee.hpp @@ -21,4 +21,7 @@ class [[eosio::contract]] sync_call_callee : public eosio::contract{ using getback_func = eosio::call_wrapper<"getback"_n, &sync_call_callee::getback>; using voidfunc_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc>; using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum>; + + using void_no_op_success_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc, eosio::access_mode::read_write, eosio::support_mode::no_op>; + using sum_no_op_success_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum, eosio::access_mode::read_write, eosio::support_mode::no_op>; }; diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index 58bb9b39cc..917cde1ba2 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -1,4 +1,5 @@ #include "sync_call_callee.hpp" +#include "sync_call_not_supported.hpp" #include #include @@ -90,6 +91,48 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ voidfunc(); } + // Verify void call. void_func uses default support_mode::abort + [[eosio::action]] + void voidfncabort() { + sync_call_not_supported::void_func void_func_abort{ "callee"_n }; + void_func_abort(); // Will throw. Tester will verify that. + } + + // void_func uses support_mode::no_op + [[eosio::action]] + void voidfncnoop() { + sync_call_not_supported::void_no_op_func void_func_no_op{ "callee"_n }; + check(void_func_no_op() == std::nullopt, "void_func_no_op did not return std::nullopt"); + } + + // verify non-void call. int_func uses default support_mode::abort + [[eosio::action]] + void intfuncabort() { + sync_call_not_supported::int_func int_func_abort{ "callee"_n }; + int_func_abort(); // Will throw. Tester will verify that. + } + + // int_func uses support_mode::no_opabort + [[eosio::action]] + void intfuncnoop() { + sync_call_not_supported::int_no_op_func int_func_no_op{ "callee"_n }; + check(int_func_no_op() == std::nullopt, "void_func_no_op did not return std::nullopt"); + } + + // void_no_op_success_func uses support_mode::no_op + [[eosio::action]] + void voidnoopsucc() { + sync_call_callee::void_no_op_success_func f{ "callee"_n }; + check(f().has_value(), "void_no_op_success_func did not return a value"); + } + + // void_no_op_success_func uses support_mode::no_op + [[eosio::action]] + void sumnoopsucc() { + sync_call_callee::sum_no_op_success_func f{ "callee"_n }; + check(*f(7, 8, 9) == 24, "sum_no_op_success_func did not return a value"); + } + [[eosio::action]] void hdrvaltest() { // Verify function name validation works diff --git a/tests/unit/test_contracts/sync_call_not_supported.cpp b/tests/unit/test_contracts/sync_call_not_supported.cpp index 0688ae4330..270c91d935 100644 --- a/tests/unit/test_contracts/sync_call_not_supported.cpp +++ b/tests/unit/test_contracts/sync_call_not_supported.cpp @@ -1,30 +1,10 @@ -#include -#include +#include "sync_call_not_supported.hpp" -// This contract does not tag any function by `call` attribute. -// No `sync_call` entry point is generated. -// Sync calls to any functions in this contract will fail. -class [[eosio::contract]] sync_call_not_supported : public eosio::contract{ -public: - using contract::contract; +[[eosio::action]] +void sync_call_not_supported::voidfunc() { +} - [[eosio::action]] - void dummy() { - } - using dummy_func = eosio::call_wrapper<"dummy"_n, &sync_call_not_supported::dummy>; - using dummy_no_op_func = eosio::call_wrapper<"dummy"_n, &sync_call_not_supported::dummy, eosio::access_mode::read_write, eosio::support_mode::no_op>; - - // request no-op - [[eosio::action]] - void noopifnot() { - dummy_no_op_func dummy_f{"caller"_n}; - dummy_f(); - } - - // request abort - [[eosio::action]] - void abortifnot() { - dummy_func dummy_f{"caller"_n}; // default is abort - dummy_f(); - } -}; +[[eosio::action]] +int sync_call_not_supported::intfunc() { + return 1; +} diff --git a/tests/unit/test_contracts/sync_call_not_supported.hpp b/tests/unit/test_contracts/sync_call_not_supported.hpp new file mode 100644 index 0000000000..273fe48328 --- /dev/null +++ b/tests/unit/test_contracts/sync_call_not_supported.hpp @@ -0,0 +1,23 @@ +#include +#include + +// Because this contract does not tag any functions by `call` attribute, +// `sync_call` entry point is not generated. +// Any sync calls to this contract will return a status indicating +// sync calls are not supported by the receiver. +class [[eosio::contract]] sync_call_not_supported : public eosio::contract{ +public: + using contract::contract; + + [[eosio::action]] + void voidfunc(); + + [[eosio::action]] + int intfunc(); + + using void_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_not_supported::voidfunc>; // default behavior: abort when called + using void_no_op_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_not_supported::voidfunc, eosio::access_mode::read_write, eosio::support_mode::no_op>; // no op when called + + using int_func = eosio::call_wrapper<"intfunc"_n, &sync_call_not_supported::intfunc>; // default behavior: abort when called + using int_no_op_func = eosio::call_wrapper<"intfunc"_n, &sync_call_not_supported::intfunc, eosio::access_mode::read_write, eosio::support_mode::no_op>; // no op when called +}; From 58bd4c0ad51fbb9d67c19c50aea197404f22dd52 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Wed, 21 May 2025 08:37:05 -0400 Subject: [PATCH 17/29] Add toolchain tests for the validation of arguments types and numbers by call_wrapper --- .../sync_call_params_num_tests.cpp | 31 +++++++++++++++++++ .../sync_call_params_num_tests.json | 11 +++++++ .../sync_call_params_type_tests.cpp | 28 +++++++++++++++++ .../sync_call_params_type_tests.json | 11 +++++++ 4 files changed, 81 insertions(+) create mode 100644 tests/toolchain/compile-fail/sync_call_params_num_tests.cpp create mode 100644 tests/toolchain/compile-fail/sync_call_params_num_tests.json create mode 100644 tests/toolchain/compile-fail/sync_call_params_type_tests.cpp create mode 100644 tests/toolchain/compile-fail/sync_call_params_type_tests.json diff --git a/tests/toolchain/compile-fail/sync_call_params_num_tests.cpp b/tests/toolchain/compile-fail/sync_call_params_num_tests.cpp new file mode 100644 index 0000000000..e61dae0879 --- /dev/null +++ b/tests/toolchain/compile-fail/sync_call_params_num_tests.cpp @@ -0,0 +1,31 @@ +#include +#include + +// Test the validation of the number of arguments passed in call_wrapper. +// Expected error: +// .../build/bin/../include/eosiolib/contracts/eosio/detail.hpp:72:7: error: static_assert failed due to requirement 'sizeof...(Ts) == std::tuple_size>::value' +// static_assert(sizeof...(Ts) == std::tuple_size>::value); + +class [[eosio::contract]] sync_call_invalid_arg_nums : public eosio::contract{ +public: + using contract::contract; + + [[eosio::call]] + uint32_t sum(uint32_t a, uint32_t b, uint32_t c) { + return a + b + c; + } + + using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_invalid_arg_nums::sum>; + + // Fewer number of arguments + [[eosio::action]] + void fewerargs() { + sum_func{"callee"_n}(1, 2); + } + + // More number of arguments + [[eosio::action]] + void moreargs() { + sum_func{"callee"_n}(1, 2, 3, 4); + } +}; diff --git a/tests/toolchain/compile-fail/sync_call_params_num_tests.json b/tests/toolchain/compile-fail/sync_call_params_num_tests.json new file mode 100644 index 0000000000..dd8c34f64c --- /dev/null +++ b/tests/toolchain/compile-fail/sync_call_params_num_tests.json @@ -0,0 +1,11 @@ +{ + "tests" : [ + { + "compile_flags": [], + "expected" : { + "exit-code": 255, + "stderr": "error: static_assert failed due to requirement 'sizeof" + } + } + ] +} diff --git a/tests/toolchain/compile-fail/sync_call_params_type_tests.cpp b/tests/toolchain/compile-fail/sync_call_params_type_tests.cpp new file mode 100644 index 0000000000..bc8bb1ad04 --- /dev/null +++ b/tests/toolchain/compile-fail/sync_call_params_type_tests.cpp @@ -0,0 +1,28 @@ +#include +#include + +// Test the validation of the types of arguments passed in call_wrapper. +// Expected error: +// build/bin/../include/eosiolib/contracts/eosio/detail.hpp:60:7: error: static_assert failed due to requirement 'detail::is_same::value' +// static_assert(detail::is_same::type, typename convert>::type>::type>::value); +class [[eosio::contract]] sync_call_invalid_arg_nums : public eosio::contract{ +public: + using contract::contract; + + [[eosio::call]] + uint32_t sum(uint32_t a, uint32_t b, uint32_t c) { + return a + b + c; + } + + using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_invalid_arg_nums::sum>; + + struct empty { + }; + + // Invalid first argument + [[eosio::action]] + void wrongrettype() { + empty bad; + sum_func{"callee"_n}(bad, 2, 3); + } +}; diff --git a/tests/toolchain/compile-fail/sync_call_params_type_tests.json b/tests/toolchain/compile-fail/sync_call_params_type_tests.json new file mode 100644 index 0000000000..c315ba6b02 --- /dev/null +++ b/tests/toolchain/compile-fail/sync_call_params_type_tests.json @@ -0,0 +1,11 @@ +{ + "tests" : [ + { + "compile_flags": [], + "expected" : { + "exit-code": 255, + "stderr": "error: static_assert failed due to requirement 'detail::is_same" + } + } + ] +} From d7fa594cc36bcf21989410548aaf7c5b56a8e9f0 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 22 May 2025 18:09:53 -0400 Subject: [PATCH 18/29] Bring llvm to latest --- cdt-llvm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdt-llvm b/cdt-llvm index 1a94400c77..3a59194188 160000 --- a/cdt-llvm +++ b/cdt-llvm @@ -1 +1 @@ -Subproject commit 1a94400c7765abd88dad5563151edf3655091888 +Subproject commit 3a5919418877b20800d31fad707740bba6704652 From aea6188d15d2b958080d51c3322621fb76b14984 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 22 May 2025 22:06:04 -0400 Subject: [PATCH 19/29] Bump cdt-llvm version to pick up entry point return status constants --- cdt-llvm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdt-llvm b/cdt-llvm index 3a59194188..4667f4959e 160000 --- a/cdt-llvm +++ b/cdt-llvm @@ -1 +1 @@ -Subproject commit 3a5919418877b20800d31fad707740bba6704652 +Subproject commit 4667f4959eed50c2f57554c2d97ebcf9f15360fb From 061efae47521c3c850d90ac17a9e9c701bc79fb1 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 08:26:52 -0400 Subject: [PATCH 20/29] Update unknown function and invalid header tests to accommodate new error code values --- tests/unit/test_contracts/sync_call_caller.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index 917cde1ba2..415dd93522 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -139,12 +139,12 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ call_data_header unkwn_func_header{ .version = 0, .func_name = "unknwnfunc"_n.value }; const std::vector unkwn_func_data{ eosio::pack(unkwn_func_header) }; // unknwnfunc is not in "callee"_n contract auto status = eosio::call("callee"_n.value, 0, unkwn_func_data.data(), unkwn_func_data.size()); - eosio::check(status == -3, "call did not return -3 for unknown function"); + eosio::check(status == -10001, "call did not return -10001 for unknown function"); // Verify version validation works call_data_header bad_version_header{ .version = 1, .func_name = "sum"_n.value }; // version 1 is not supported const std::vector bad_version_data{ eosio::pack(bad_version_header) }; status = eosio::call("callee"_n.value, 0, bad_version_data.data(), bad_version_data.size()); - eosio::check(status == -2, "call did not return -2 for invalid version"); + eosio::check(status == -10000, "call did not return -10000 for invalid version"); } }; From aa16dcd65ad0d89b4573f8d6c21e650574039409 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 14:41:19 -0400 Subject: [PATCH 21/29] Use enum class instead of plain enum to define enums --- libraries/eosiolib/contracts/eosio/call.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index bb7b6aa2d9..aed64be4f2 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -52,11 +52,11 @@ namespace eosio { } // Indicate whether a sync call is read_write or read_only. Default is read_write - enum access_mode { read_write = 0, read_only = 1 }; + enum class access_mode { read_write = 0, read_only = 1 }; // Indicate the action to take if the receiver does not support sync calls. - // Default is abort - enum support_mode { abort = 0, no_op = 1 }; + // Default is abort_op + enum class support_mode { abort_op = 0, no_op = 1 }; // For a void function, when support_mode is set to no_op, the call_wrapper. // returns `std::optional`. If the optional has no value, it indicates @@ -87,7 +87,7 @@ namespace eosio { * get(); * @endcode */ - template + template struct call_wrapper { template constexpr call_wrapper(Receiver&& receiver) @@ -100,7 +100,7 @@ namespace eosio { using orig_ret_type = typename detail::function_traits::return_type; using return_type = std::conditional_t< - Support_Mode == support_mode::abort, // if Support_Mode is abort + Support_Mode == support_mode::abort_op,// if Support_Mode is abort_op orig_ret_type, // use the original return type std::conditional_t< // else std::is_void::value, // original return type is void @@ -126,8 +126,8 @@ namespace eosio { auto ret_val_size = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); if (ret_val_size < 0) { // the receiver does not support sync calls - if constexpr (Support_Mode == support_mode::abort) { - check(false, "receiver does not support sync call but support_mode is set to abort"); + if constexpr (Support_Mode == support_mode::abort_op) { + check(false, "receiver does not support sync call but support_mode is set to abort_op"); } else { return std::nullopt; } From 090c99fd66035827cdd73eb91d281f9e13d493f3 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 14:43:06 -0400 Subject: [PATCH 22/29] Use std::forward_as_tuple instead of std::make_tuple to avoid making a copy --- libraries/eosiolib/contracts/eosio/call.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index aed64be4f2..34ff99eaa4 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -121,7 +121,7 @@ namespace eosio { call_data_header header{ .version = 0, .func_name = function_name.value }; - const std::vector data{ pack(std::make_tuple(header, detail::deduced{std::forward(args)...})) }; + const std::vector data{ pack(std::forward_as_tuple(header, detail::deduced{std::forward(args)...})) }; auto ret_val_size = internal_use_do_not_use::call(receiver.value, flags, data.data(), data.size()); From 0acd966180db834a2e1c656fc99f1c7a679f0bee Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 14:44:41 -0400 Subject: [PATCH 23/29] Clarify the comment about why free() is not called --- libraries/eosiolib/contracts/eosio/call.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 34ff99eaa4..318885728e 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -141,7 +141,7 @@ namespace eosio { return void_call{}; } else { constexpr size_t max_stack_buffer_size = 512; - char* buffer = (char*)(max_stack_buffer_size < ret_val_size ? malloc(ret_val_size) : alloca(ret_val_size)); // intentionally no `free()` is called. the memory will be reset after execution + char* buffer = (char*)(max_stack_buffer_size < ret_val_size ? malloc(ret_val_size) : alloca(ret_val_size)); // intentionally no `free()` is called. the memory will be freed at the end of callers wasm execution. internal_use_do_not_use::get_call_return_value(buffer, ret_val_size); auto ret_val = unpack(buffer, ret_val_size); From 91b4b00ebe9336e86a2b8a68bdafd48582811f16 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 14:57:28 -0400 Subject: [PATCH 24/29] Use orig_ret_type explicitly to make sure return value optimization is used --- libraries/eosiolib/contracts/eosio/call.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 318885728e..16d61bb147 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -143,7 +143,7 @@ namespace eosio { constexpr size_t max_stack_buffer_size = 512; char* buffer = (char*)(max_stack_buffer_size < ret_val_size ? malloc(ret_val_size) : alloca(ret_val_size)); // intentionally no `free()` is called. the memory will be freed at the end of callers wasm execution. internal_use_do_not_use::get_call_return_value(buffer, ret_val_size); - auto ret_val = unpack(buffer, ret_val_size); + orig_ret_type ret_val = unpack(buffer, ret_val_size); if constexpr (Support_Mode == support_mode::no_op) { return std::make_optional(ret_val); From d51ad33ef52ad216e30e6fc581702eaf68ca05f8 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 15:20:42 -0400 Subject: [PATCH 25/29] Use eosio::name instead of uint64_t for receiver in host function call; it is much cleanner --- libraries/eosiolib/contracts/eosio/call.hpp | 4 ++-- tests/unit/test_contracts/sync_call_caller.cpp | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index 16d61bb147..e6c1513fcb 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -35,8 +35,8 @@ namespace eosio { * @note There are some methods from the @ref call that can be used directly from C++ */ - inline int64_t call(uint64_t receiver, uint64_t flags, const char* data, size_t data_size) { - return internal_use_do_not_use::call(receiver, flags, data, data_size); + inline int64_t call(eosio::name receiver, uint64_t flags, const char* data, size_t data_size) { + return internal_use_do_not_use::call(receiver.value, flags, data, data_size); } inline uint32_t get_call_return_value( void* mem, uint32_t len ) { diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index 415dd93522..eae513bfe3 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -15,7 +15,7 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ void hstretvaltst() { call_data_header header{ .version = 0, .func_name = "getten"_n.value }; const std::vector data{ eosio::pack(header) }; - auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); + auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); std::vector return_value; @@ -38,7 +38,7 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ // `getback(uint32_t p)` returns p call_data_header header{ .version = 0, .func_name = "getback"_n.value }; const std::vector data{ eosio::pack(std::make_tuple(header, 5)) }; - auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); + auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); std::vector return_value; @@ -60,7 +60,7 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ void hstmulprmtst() { call_data_header header{ .version = 0, .func_name = "sum"_n.value }; const std::vector data{ eosio::pack(std::make_tuple(header, 10, 20, 30)) }; - auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); + auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size()); eosio::check(expected_size >= 0, "call did not return a positive value"); std::vector return_value; @@ -81,7 +81,7 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ void hstvodfuntst() { call_data_header header{ .version = 0, .func_name = "voidfunc"_n.value }; const std::vector data{ eosio::pack(header) }; - auto expected_size = eosio::call("callee"_n.value, 0, data.data(), data.size()); + auto expected_size = eosio::call("callee"_n, 0, data.data(), data.size()); eosio::check(expected_size == 0, "call did not return 0"); // void function. return value size should be 0 } @@ -138,13 +138,13 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ // Verify function name validation works call_data_header unkwn_func_header{ .version = 0, .func_name = "unknwnfunc"_n.value }; const std::vector unkwn_func_data{ eosio::pack(unkwn_func_header) }; // unknwnfunc is not in "callee"_n contract - auto status = eosio::call("callee"_n.value, 0, unkwn_func_data.data(), unkwn_func_data.size()); + auto status = eosio::call("callee"_n, 0, unkwn_func_data.data(), unkwn_func_data.size()); eosio::check(status == -10001, "call did not return -10001 for unknown function"); // Verify version validation works call_data_header bad_version_header{ .version = 1, .func_name = "sum"_n.value }; // version 1 is not supported const std::vector bad_version_data{ eosio::pack(bad_version_header) }; - status = eosio::call("callee"_n.value, 0, bad_version_data.data(), bad_version_data.size()); + status = eosio::call("callee"_n, 0, bad_version_data.data(), bad_version_data.size()); eosio::check(status == -10000, "call did not return -10000 for invalid version"); } }; From a3ad5f6f7803509d8001cf19263f2cf45b41a91a Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 15:35:37 -0400 Subject: [PATCH 26/29] Change spring-dev branch back to sync_call from the temporary return_status branch --- .cicd/defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cicd/defaults.json b/.cicd/defaults.json index f35219f5c8..8f75428a37 100644 --- a/.cicd/defaults.json +++ b/.cicd/defaults.json @@ -1,6 +1,6 @@ { "antelope-spring-dev":{ - "target":"return_status", + "target":"sync_call", "prerelease":false } } From e1227b7767788b8fb74e26f06deb364005d71539 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Fri, 23 May 2025 15:44:38 -0400 Subject: [PATCH 27/29] Add a comment why function name type in call_data_header is uint64_t, not eosio::name --- libraries/eosiolib/contracts/eosio/call.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/eosiolib/contracts/eosio/call.hpp b/libraries/eosiolib/contracts/eosio/call.hpp index e6c1513fcb..a27ed3b46a 100644 --- a/libraries/eosiolib/contracts/eosio/call.hpp +++ b/libraries/eosiolib/contracts/eosio/call.hpp @@ -67,7 +67,7 @@ namespace eosio { struct call_data_header { uint32_t version = 0; - uint64_t func_name = 0; + uint64_t func_name = 0; // At WASM level, function name is an uint64_t. We do not use eosio::name here to make the decoding function name simpler in sync_call entry point function. EOSLIB_SERIALIZE(call_data_header, (version)(func_name)) }; @@ -75,7 +75,7 @@ namespace eosio { /** * Wrapper for simplifying making a sync call * - * @brief Used to wrap an a particular sync call to simplify the process of other contracts making sync calls to the "wrapped" call. + * @brief Used to wrap a particular sync call to simplify the process of other contracts making sync calls to the "wrapped" call. * Example: * @code * // defined by contract writer of the sync call functions From 6abb8dbb268427a8ccb32272b07765a420a5aeec Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Mon, 26 May 2025 16:17:57 -0400 Subject: [PATCH 28/29] Update cdt-llvm to pickup generate sync call entry point function only when one is not provided --- cdt-llvm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdt-llvm b/cdt-llvm index 4667f4959e..ff3f15b471 160000 --- a/cdt-llvm +++ b/cdt-llvm @@ -1 +1 @@ -Subproject commit 4667f4959eed50c2f57554c2d97ebcf9f15360fb +Subproject commit ff3f15b47113a20fd0d8e695de6350a91e46cd3d From bc5974f2a0a32753de2a8413579a126522a10e0f Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Sat, 31 May 2025 14:12:56 -0400 Subject: [PATCH 29/29] Add tests for complex parameter passing (a mix of structs and integer) --- tests/integration/call_tests.cpp | 20 +++++++++++++ .../unit/test_contracts/sync_call_callee.cpp | 11 +++++++ .../unit/test_contracts/sync_call_callee.hpp | 23 +++++++++++++++ .../unit/test_contracts/sync_call_caller.cpp | 29 +++++++++++++++++-- 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/tests/integration/call_tests.cpp b/tests/integration/call_tests.cpp index 62f9c1bf1d..e217cd71dd 100644 --- a/tests/integration/call_tests.cpp +++ b/tests/integration/call_tests.cpp @@ -76,6 +76,26 @@ BOOST_AUTO_TEST_CASE(multiple_params_test) { try { BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "wrpmulprmtst"_n, "caller"_n, {})); } FC_LOG_AND_RETHROW() } +// Verify passing a struct parameter works correctly +BOOST_AUTO_TEST_CASE(struct_param_test) { try { + call_tester t({ + {"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()}, + {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} + }); + + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "structtest"_n, "caller"_n, {})); +} FC_LOG_AND_RETHROW() } + +// Verify passing a mix of structs and integer works correctly +BOOST_AUTO_TEST_CASE(mix_struct_int_params_test) { try { + call_tester t({ + {"caller"_n, contracts::caller_wasm(), contracts::caller_abi().data()}, + {"callee"_n, contracts::callee_wasm(), contracts::callee_abi().data()} + }); + + BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "structinttst"_n, "caller"_n, {})); +} FC_LOG_AND_RETHROW() } + // Verify a sync call to a void function works properly. BOOST_AUTO_TEST_CASE(void_func_test) { try { call_tester t({ diff --git a/tests/unit/test_contracts/sync_call_callee.cpp b/tests/unit/test_contracts/sync_call_callee.cpp index 863658bb33..2715083808 100644 --- a/tests/unit/test_contracts/sync_call_callee.cpp +++ b/tests/unit/test_contracts/sync_call_callee.cpp @@ -20,3 +20,14 @@ void sync_call_callee::voidfunc() { uint32_t sync_call_callee::sum(uint32_t a, uint32_t b, uint32_t c) { return a + b + c; } + +[[eosio::call]] +struct1_t sync_call_callee::structonly(struct1_t s) { + return s; +} + +[[eosio::call]] +struct1_t sync_call_callee::structmix(struct1_t s1, int32_t m, struct2_t s2) { + return { .a = s1.a * m + s2.c, .b = s1.b * m + s2.d }; +} + diff --git a/tests/unit/test_contracts/sync_call_callee.hpp b/tests/unit/test_contracts/sync_call_callee.hpp index 87b7a02f93..4aaa660ebb 100644 --- a/tests/unit/test_contracts/sync_call_callee.hpp +++ b/tests/unit/test_contracts/sync_call_callee.hpp @@ -1,6 +1,18 @@ #include #include +struct struct1_t { + int64_t a; + uint64_t b; +}; + +struct struct2_t { + char a; + bool b; + int64_t c; + uint64_t d; +}; + class [[eosio::contract]] sync_call_callee : public eosio::contract{ public: using contract::contract; @@ -17,10 +29,21 @@ class [[eosio::contract]] sync_call_callee : public eosio::contract{ [[eosio::action, eosio::call]] uint32_t sum(uint32_t a, uint32_t b, uint32_t c); + // pass in a struct and return it + [[eosio::call]] + struct1_t structonly(struct1_t s); + + // pass in two structs and an integer, multiply each field in the struct by + // the integer, add last two fields of the second struct, and return the result + [[eosio::call]] + struct1_t structmix(struct1_t s1, int32_t m, struct2_t s2); + using getten_func = eosio::call_wrapper<"getten"_n, &sync_call_callee::getten>; using getback_func = eosio::call_wrapper<"getback"_n, &sync_call_callee::getback>; using voidfunc_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc>; using sum_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum>; + using structonly_func = eosio::call_wrapper<"structonly"_n, &sync_call_callee::structonly>; + using structmix_func = eosio::call_wrapper<"structmix"_n, &sync_call_callee::structmix>; using void_no_op_success_func = eosio::call_wrapper<"voidfunc"_n, &sync_call_callee::voidfunc, eosio::access_mode::read_write, eosio::support_mode::no_op>; using sum_no_op_success_func = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum, eosio::access_mode::read_write, eosio::support_mode::no_op>; diff --git a/tests/unit/test_contracts/sync_call_caller.cpp b/tests/unit/test_contracts/sync_call_caller.cpp index eae513bfe3..7d082bedd3 100644 --- a/tests/unit/test_contracts/sync_call_caller.cpp +++ b/tests/unit/test_contracts/sync_call_caller.cpp @@ -67,14 +67,39 @@ class [[eosio::contract]] sync_call_caller : public eosio::contract{ return_value.resize(expected_size); auto actual_size = eosio::get_call_return_value(return_value.data(), return_value.size()); eosio::check(actual_size == expected_size, "actual_size not equal to expected_size"); - eosio::check(eosio::unpack(return_value) == 60u, "sum of 10, 20, an 30 not 60"); // sum returns the sum of the 3 arguments + eosio::check(eosio::unpack(return_value) == 60u, "sum of 10, 20, and 30 not 60"); // sum returns the sum of the 3 arguments } // Using call_wrapper, testing multiple parameters passing [[eosio::action]] void wrpmulprmtst() { sync_call_callee::sum_func sum{ "callee"_n }; - eosio::check(sum(10, 20, 30) == 60u, "sum of 10, 20, an 30 not 60"); + eosio::check(sum(10, 20, 30) == 60u, "sum of 10, 20, and 30 not 60"); + } + + // Verify single struct parameter passing + [[eosio::action]] + void structtest() { + sync_call_callee::structonly_func func{ "callee"_n }; + struct1_t input = { 10, 20 }; + auto output = func(input); // structonly_func returns the input as is + eosio::check(output.a == input.a, "field a in output is not equal to a in input"); + eosio::check(output.b == input.b, "field b in output is not equal to b in input"); + } + + // Verify mix of struct and integer parameters passing + [[eosio::action]] + void structinttst() { + sync_call_callee::structmix_func func{ "callee"_n }; + struct1_t input1 = { 10, 20 }; + struct2_t input2 = { 'a', true, 50, 100 }; + int32_t m = 2; + + // structmix_func multiply each field of input1 by m, + // add last two fields of input2, and return a struct1_t + auto output = func(input1, m, input2); + eosio::check(output.a == m * input1.a + input2.c, "field a of output is not correct"); + eosio::check(output.b == m * input1.b + input2.d, "field b of output is not correct"); } [[eosio::action]]