diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index df5d2c8bde..ca586a3af6 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1595,12 +1595,14 @@ struct controller_impl { trx->packed_trx()->get_prunable_size() ); } + trx_context.delay = fc::seconds(trn.delay_sec); + if( check_auth ) { authorization.check_authorization( trn.actions, trx->recovered_keys(), {}, - fc::seconds(trn.delay_sec), + trx_context.delay, [&trx_context](){ trx_context.checktime(); }, false, trx->is_dry_run() @@ -1613,7 +1615,9 @@ struct controller_impl { trx->billed_cpu_time_us = trx_context.billed_cpu_time_us; if (!trx->implicit() && !trx->is_read_only()) { - transaction_receipt::status_enum s = transaction_receipt::executed; + transaction_receipt::status_enum s = (trx_context.delay == fc::seconds(0)) + ? transaction_receipt::executed + : transaction_receipt::delayed; trace->receipt = push_receipt(*trx->packed_trx(), s, trx_context.billed_cpu_time_us, trace->net_usage); std::get(pending->_block_stage)._pending_trx_metas.emplace_back(trx); } else { diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index cef4b94e34..f03d61a1f1 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -370,7 +370,6 @@ namespace eosio { namespace chain { private: friend class apply_context; friend class transaction_context; - friend void modify_gto_for_canceldelay_test(controller& control, const transaction_id_type& trx_id); // canceldelay_test in delay_tests.cpp need access to mutable_db chainbase::database& mutable_db()const; diff --git a/libraries/chain/include/eosio/chain/transaction_context.hpp b/libraries/chain/include/eosio/chain/transaction_context.hpp index 18d3c31e65..430defce27 100644 --- a/libraries/chain/include/eosio/chain/transaction_context.hpp +++ b/libraries/chain/include/eosio/chain/transaction_context.hpp @@ -112,6 +112,7 @@ namespace eosio { namespace chain { void execute_action( uint32_t action_ordinal, uint32_t recurse_depth ); + void schedule_transaction(); void record_transaction( const transaction_id_type& id, fc::time_point_sec expire ); void validate_cpu_usage_to_bill( int64_t billed_us, int64_t account_cpu_limit, bool check_minimum, int64_t subjective_billed_us )const; @@ -142,6 +143,7 @@ namespace eosio { namespace chain { /// the maximum number of virtual CPU instructions of the transaction that can be safely billed to the billable accounts uint64_t initial_max_billable_cpu = 0; + fc::microseconds delay; bool is_input = false; bool apply_context_free = true; bool enforce_whiteblacklist = true; diff --git a/libraries/chain/transaction_context.cpp b/libraries/chain/transaction_context.cpp index cd6b872955..262d7995a7 100644 --- a/libraries/chain/transaction_context.cpp +++ b/libraries/chain/transaction_context.cpp @@ -247,7 +247,12 @@ namespace eosio { namespace chain { uint64_t packed_trx_prunable_size ) { const transaction& trx = packed_trx.get_transaction(); - EOS_ASSERT( trx.delay_sec.value == 0, transaction_exception, "transaction cannot be delayed" ); + // delayed transactions are not allowed after protocol feature + // DISABLE_DEFERRED_TRXS_STAGE_1 is activated; + // read-only and dry-run transactions are not allowed to be delayed at any time + if( control.is_builtin_activated(builtin_protocol_feature_t::disable_deferred_trxs_stage_1) || is_transient() ) { + EOS_ASSERT( trx.delay_sec.value == 0, transaction_exception, "transaction cannot be delayed" ); + } if( trx.transaction_extensions.size() > 0 ) { disallow_transaction_extensions( "no transaction extensions supported yet for input transactions" ); } @@ -266,6 +271,13 @@ namespace eosio { namespace chain { uint64_t initial_net_usage = static_cast(cfg.base_per_transaction_net_usage) + packed_trx_unprunable_size + discounted_size_for_pruned_data; + if( trx.delay_sec.value > 0 ) { + // If delayed, also charge ahead of time for the additional net usage needed to retire the delayed transaction + // whether that be by successfully executing, soft failure, hard failure, or expiration. + initial_net_usage += static_cast(cfg.base_per_transaction_net_usage) + + static_cast(config::transaction_id_net_usage); + } + published = control.pending_block_time(); is_input = true; if (!control.skip_trx_checks()) { @@ -309,8 +321,10 @@ namespace eosio { namespace chain { } } - for( const auto& act : trx.actions ) { - schedule_action( act, act.account, false, 0, 0 ); + if( delay == fc::microseconds() ) { + for( const auto& act : trx.actions ) { + schedule_action( act, act.account, false, 0, 0 ); + } } auto& action_traces = trace->action_traces; @@ -318,6 +332,10 @@ namespace eosio { namespace chain { for( uint32_t i = 1; i <= num_original_actions_to_execute; ++i ) { execute_action( i, 0 ); } + + if( delay != fc::microseconds() ) { + schedule_transaction(); + } } void transaction_context::finalize() { @@ -715,6 +733,42 @@ namespace eosio { namespace chain { acontext.exec(); } + void transaction_context::schedule_transaction() { + // Charge ahead of time for the additional net usage needed to retire the delayed transaction + // whether that be by successfully executing, soft failure, hard failure, or expiration. + const transaction& trx = packed_trx.get_transaction(); + if( trx.delay_sec.value == 0 ) { // Do not double bill. Only charge if we have not already charged for the delay. + const auto& cfg = control.get_global_properties().configuration; + add_net_usage( static_cast(cfg.base_per_transaction_net_usage) + + static_cast(config::transaction_id_net_usage) ); // Will exit early if net usage cannot be payed. + } + + auto first_auth = trx.first_authorizer(); + + uint32_t trx_size = 0; + const auto& cgto = control.mutable_db().create( [&]( auto& gto ) { + gto.trx_id = id; + gto.payer = first_auth; + gto.sender = account_name(); /// delayed transactions have no sender + gto.sender_id = transaction_id_to_sender_id( gto.trx_id ); + gto.published = control.pending_block_time(); + gto.delay_until = gto.published + delay; + gto.expiration = gto.delay_until + fc::seconds(control.get_global_properties().configuration.deferred_trx_expiration_window); + trx_size = gto.set( trx ); + + if (auto dm_logger = control.get_deep_mind_logger(is_transient())) { + std::string event_id = RAM_EVENT_ID("${id}", ("id", gto.id)); + + dm_logger->on_create_deferred(deep_mind_handler::operation_qualifier::push, gto, packed_trx); + dm_logger->on_ram_trace(std::move(event_id), "deferred_trx", "push", "deferred_trx_pushed"); + } + }); + + int64_t ram_delta = (config::billable_size_v + trx_size); + add_ram_usage( cgto.payer, ram_delta ); + trace->account_ram_delta = account_delta( cgto.payer, ram_delta ); + } + void transaction_context::record_transaction( const transaction_id_type& id, fc::time_point_sec expire ) { try { control.mutable_db().create([&](transaction_object& transaction) { diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index b53090a7a6..d574254c73 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -773,6 +773,10 @@ class producer_plugin_impl : public std::enable_shared_from_this next) { + + const transaction& t = trx->get_transaction(); + EOS_ASSERT( t.delay_sec.value == 0, transaction_exception, "transaction cannot be delayed" ); + if (trx_type == transaction_metadata::trx_type::read_only) { assert(_ro_thread_pool_size > 0); // enforced by chain_plugin assert(app().executor().get_main_thread_id() != std::this_thread::get_id()); // should only be called from read only threads diff --git a/plugins/producer_plugin/test/CMakeLists.txt b/plugins/producer_plugin/test/CMakeLists.txt index 42c42596f8..877ffd9f11 100644 --- a/plugins/producer_plugin/test/CMakeLists.txt +++ b/plugins/producer_plugin/test/CMakeLists.txt @@ -2,7 +2,8 @@ add_executable( test_producer_plugin test_trx_full.cpp test_options.cpp test_block_timing_util.cpp + test_disallow_delayed_trx.cpp main.cpp ) target_link_libraries( test_producer_plugin producer_plugin eosio_testing eosio_chain_wrap ) -add_test(NAME test_producer_plugin COMMAND plugins/producer_plugin/test/test_producer_plugin WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) \ No newline at end of file +add_test(NAME test_producer_plugin COMMAND plugins/producer_plugin/test/test_producer_plugin WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/plugins/producer_plugin/test/test_disallow_delayed_trx.cpp b/plugins/producer_plugin/test/test_disallow_delayed_trx.cpp new file mode 100644 index 0000000000..7189fb4e1a --- /dev/null +++ b/plugins/producer_plugin/test/test_disallow_delayed_trx.cpp @@ -0,0 +1,101 @@ +#include +#include +#include + +namespace eosio::test::detail { +using namespace eosio::chain::literals; +struct testit { + uint64_t id; + + testit( uint64_t id = 0 ) :id(id){} + + static account_name get_account() { + return chain::config::system_account_name; + } + + static action_name get_name() { + return "testit"_n; + } +}; +} +FC_REFLECT( eosio::test::detail::testit, (id) ) + +namespace { + +using namespace eosio; +using namespace eosio::chain; +using namespace eosio::test::detail; + +auto make_delayed_trx( const chain_id_type& chain_id ) { + account_name creator = config::system_account_name; + + signed_transaction trx; + trx.actions.emplace_back( vector{{creator, config::active_name}}, testit{0} ); + trx.delay_sec = 10; + auto priv_key = private_key_type::regenerate(fc::sha256::hash(std::string("nathan"))); + trx.sign( priv_key, chain_id ); + + return std::make_shared( std::move(trx) ); +} +} + +BOOST_AUTO_TEST_SUITE(disallow_delayed_trx_test) + +// Verifies that incoming delayed transactions are blocked. +BOOST_AUTO_TEST_CASE(delayed_trx) { + using namespace std::chrono_literals; + fc::temp_directory temp; + appbase::scoped_app app; + auto temp_dir_str = temp.path().string(); + + std::promise> plugin_promise; + std::future> plugin_fut = plugin_promise.get_future(); + std::thread app_thread( [&]() { + try { + fc::logger::get(DEFAULT_LOGGER).set_log_level(fc::log_level::debug); + std::vector argv = + {"test", "--data-dir", temp_dir_str.c_str(), "--config-dir", temp_dir_str.c_str(), + "-p", "eosio", "-e", "--disable-subjective-p2p-billing=true" }; + app->initialize( argv.size(), (char**) &argv[0] ); + app->startup(); + plugin_promise.set_value( + {app->find_plugin(), app->find_plugin()} ); + app->exec(); + return; + } FC_LOG_AND_DROP() + BOOST_CHECK(!"app threw exception see logged error"); + } ); + + auto[prod_plug, chain_plug] = plugin_fut.get(); + auto chain_id = chain_plug->get_chain_id(); + + // create a delayed trx + auto ptrx = make_delayed_trx( chain_id ); + + // send it as incoming trx + app->post( priority::low, [ptrx, &app]() { + bool return_failure_traces = true; + + // the delayed trx is blocked + BOOST_REQUIRE_EXCEPTION( + app->get_method()(ptrx, + false, + transaction_metadata::trx_type::input, + return_failure_traces, + [ptrx, return_failure_traces] (const next_function_variant& result) { + elog( "trace with except ${e}", ("e", fc::json::to_pretty_string( *std::get( result ) )) ); + } + ), + fc::exception, + eosio::testing::fc_exception_message_starts_with("transaction cannot be delayed") + ); + }); + + // leave time for transaction to be executed + std::this_thread::sleep_for( 2000ms ); + + app->quit(); + app_thread.join(); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/nodeos_chainbase_allocation_test.py b/tests/nodeos_chainbase_allocation_test.py index 5771428b80..4c2ec8ee21 100755 --- a/tests/nodeos_chainbase_allocation_test.py +++ b/tests/nodeos_chainbase_allocation_test.py @@ -31,7 +31,6 @@ # The following is the list of chainbase objects that need to be verified: # - account_object (bootstrap) # - code_object (bootstrap) - # - generated_transaction_object # - global_property_object # - key_value_object (bootstrap) # - protocol_state_object (bootstrap) @@ -55,12 +54,6 @@ irrNode = cluster.getNode(irrNodeId) nonProdNode = cluster.getNode(nonProdNodeId) - # Create delayed transaction to create "generated_transaction_object" - cmd = "create account -j eosio sample EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV\ - EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV --delay-sec 600 -p eosio" - trans = producerNode.processCleosCmd(cmd, cmd, silentErrors=False) - assert trans - # Schedule a new producer to trigger new producer schedule for "global_property_object" newProducerAcc = Account("newprod") newProducerAcc.ownerPublicKey = "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" diff --git a/unittests/api_tests.cpp b/unittests/api_tests.cpp index 018e32d0ac..20eced06f6 100644 --- a/unittests/api_tests.cpp +++ b/unittests/api_tests.cpp @@ -767,11 +767,11 @@ BOOST_FIXTURE_TEST_CASE(cfa_stateful_api, validating_tester) try { BOOST_REQUIRE_EQUAL( validate(), true ); } FC_LOG_AND_RETHROW() -BOOST_FIXTURE_TEST_CASE(deferred_cfa_not_allowed, validating_tester) try { +BOOST_FIXTURE_TEST_CASE(deferred_cfa_failed, validating_tester_no_disable_deferred_trx) try { create_account( "testapi"_n ); - produce_blocks(1); - set_code( "testapi"_n, test_contracts::test_api_wasm() ); + produce_blocks(1); + set_code( "testapi"_n, test_contracts::test_api_wasm() ); account_name a = "testapi2"_n; account_name creator = config::system_account_name; @@ -785,15 +785,58 @@ BOOST_FIXTURE_TEST_CASE(deferred_cfa_not_allowed, validating_tester) try { .owner = authority( get_public_key( a, "owner" ) ), .active = authority( get_public_key( a, "active" ) ) }); - action act({}, test_api_action{}); + action act({}, test_api_action{}); trx.context_free_actions.push_back(act); - set_transaction_headers(trx, 10, 2); // set delay_sec to 2 + set_transaction_headers(trx, 10, 2); trx.sign( get_private_key( creator, "active" ), control->get_chain_id() ); + BOOST_CHECK_EXCEPTION(push_transaction( trx ), fc::exception, [&](const fc::exception &e) { - // any incoming trx is blocked - return expect_assert_message(e, "transaction cannot be delayed"); + return expect_assert_message(e, "only context free api's can be used in this context"); + }); + + produce_blocks(10); + + // CFA failed, testapi2 not created + create_account( "testapi2"_n ); + + BOOST_REQUIRE_EQUAL( validate(), true ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(deferred_cfa_success, validating_tester_no_disable_deferred_trx) try { + + create_account( "testapi"_n ); + produce_blocks(1); + set_code( "testapi"_n, test_contracts::test_api_wasm() ); + + account_name a = "testapi2"_n; + account_name creator = config::system_account_name; + signed_transaction trx; + trx.actions.emplace_back( vector{{creator,config::active_name}}, + newaccount{ + .creator = creator, + .name = a, + .owner = authority( get_public_key( a, "owner" ) ), + .active = authority( get_public_key( a, "active" ) ) + }); + action act({}, test_api_action{}); + trx.context_free_actions.push_back(act); + set_transaction_headers(trx, 10, 2); + trx.sign( get_private_key( creator, "active" ), control->get_chain_id() ); + auto trace = push_transaction( trx ); + BOOST_REQUIRE(trace != nullptr); + if (trace) { + BOOST_REQUIRE_EQUAL(transaction_receipt_header::status_enum::delayed, trace->receipt->status); + BOOST_REQUIRE_EQUAL(1, trace->action_traces.size()); + } + produce_blocks(10); + + // CFA success, testapi2 created + BOOST_CHECK_EXCEPTION(create_account( "testapi2"_n ), fc::exception, + [&](const fc::exception &e) { + return expect_assert_message(e, "Cannot create account named testapi2, as that name is already taken"); }); + BOOST_REQUIRE_EQUAL( validate(), true ); } FC_LOG_AND_RETHROW() BOOST_AUTO_TEST_CASE(light_validation_skip_cfa) try { diff --git a/unittests/delay_tests.cpp b/unittests/delay_tests.cpp index f62e9873a2..2a98ba3db9 100644 --- a/unittests/delay_tests.cpp +++ b/unittests/delay_tests.cpp @@ -16,22 +16,6 @@ using mvo = fc::mutable_variant_object; const std::string eosio_token = name("eosio.token"_n).to_string(); -// Native action hardcodes sender empty and builds sender_id from trx id. -// This method modifies those two fields for contract generated deferred -// trxs so canceldelay can be tested by canceldelay_test. -namespace eosio::chain { -inline void modify_gto_for_canceldelay_test(controller& control, const transaction_id_type& trx_id) { - auto gto = control.mutable_db().find(trx_id); - if (gto) { - control.mutable_db().modify(*gto, [&]( auto& gtx ) { - gtx.sender = account_name(); - - fc::uint128 _id(trx_id._hash[3], trx_id._hash[2]); - gtx.sender_id = (unsigned __int128)_id; - }); - } -}} /// namespace eosio::chain - static void create_accounts(validating_tester& chain) { chain.produce_blocks(); chain.create_accounts({"eosio.msig"_n, "eosio.token"_n}); @@ -190,18 +174,18 @@ static asset get_currency_balance(const validating_tester& chain, account_name a BOOST_AUTO_TEST_SUITE(delay_tests) -// Delayed trxs are blocked. -BOOST_FIXTURE_TEST_CASE( delayed_trx_blocked, validating_tester ) { try { +BOOST_FIXTURE_TEST_CASE( delay_error_create_account, validating_tester_no_disable_deferred_trx) { try { + produce_blocks(2); signed_transaction trx; account_name a = "newco"_n; account_name creator = config::system_account_name; - auto owner_auth = authority( get_public_key( a, "owner" ) ); + auto owner_auth = authority( get_public_key( a, "owner" ) ); trx.actions.emplace_back( vector{{creator,config::active_name}}, newaccount{ - .creator = creator, + .creator = "bad"_n, /// a does not exist, this should error when execute .name = a, .owner = owner_auth, .active = authority( get_public_key( a, "active" ) ) @@ -210,42 +194,21 @@ BOOST_FIXTURE_TEST_CASE( delayed_trx_blocked, validating_tester ) { try { trx.delay_sec = 3; trx.sign( get_private_key( creator, "active" ), control->get_chain_id() ); - // delayed trx is blocked - BOOST_CHECK_EXCEPTION(push_transaction( trx ), fc::exception, - [&](const fc::exception &e) { - return expect_assert_message(e, "transaction cannot be delayed"); - }); - - // no deferred trx was generated - auto gen_size = control->db().get_index().size(); - BOOST_REQUIRE_EQUAL(0u, gen_size); -} FC_LOG_AND_RETHROW() }/// delayed_trx_blocked + ilog( fc::json::to_pretty_string(trx) ); + auto trace = push_transaction( trx ); + edump((*trace)); -// Delayed actions are blocked. -BOOST_AUTO_TEST_CASE( delayed_action_blocked ) { try { - validating_tester chain; - const auto& tester_account = "tester"_n; + produce_blocks(6); - chain.create_account("tester"_n); - chain.produce_blocks(); + auto scheduled_trxs = get_scheduled_transactions(); + BOOST_REQUIRE_EQUAL(scheduled_trxs.size(), 1u); - // delayed action is blocked - BOOST_CHECK_EXCEPTION( - chain.push_action(config::system_account_name, updateauth::get_name(), tester_account, fc::mutable_variant_object() - ("account", "tester") - ("permission", "first") - ("parent", "active") - ("auth", authority(chain.get_public_key(tester_account, "first"))), - 20, 10), - fc::exception, - [&](const fc::exception &e) { - return expect_assert_message(e, "transaction cannot be delayed"); - }); + auto billed_cpu_time_us = control->get_global_properties().configuration.min_transaction_cpu_usage; + auto dtrace = control->push_scheduled_transaction(scheduled_trxs.front(), fc::time_point::maximum(), fc::microseconds::maximum(), billed_cpu_time_us, true); + BOOST_REQUIRE_EQUAL(dtrace->except.has_value(), true); + BOOST_REQUIRE_EQUAL(dtrace->except->code(), missing_auth_exception::code_value); - // no deferred trx was generated - auto gen_size = chain.control->db().get_index().size(); - BOOST_REQUIRE_EQUAL(0u, gen_size); -} FC_LOG_AND_RETHROW() }/// delayed_action_blocked +} FC_LOG_AND_RETHROW() } // test link to permission with delay directly on it BOOST_AUTO_TEST_CASE( link_delay_direct_test ) { try { @@ -1391,50 +1354,241 @@ BOOST_AUTO_TEST_CASE( canceldelay_test ) { try { validating_tester_no_disable_deferred_trx chain; chain.produce_block(); - const auto& contract_account = account_name("defcontract"); - const auto& test_account = account_name("tester"); + const auto& tester_account = "tester"_n; + std::vector ids; chain.produce_blocks(); - chain.create_accounts({contract_account, test_account}); + chain.create_account("eosio.token"_n); + chain.produce_blocks(10); + + chain.set_code("eosio.token"_n, test_contracts::eosio_token_wasm()); + chain.set_abi("eosio.token"_n, test_contracts::eosio_token_abi()); + chain.produce_blocks(); - chain.set_code(contract_account, test_contracts::deferred_test_wasm()); - chain.set_abi(contract_account, test_contracts::deferred_test_abi()); + chain.create_account("tester"_n); + chain.create_account("tester2"_n); + chain.produce_blocks(10); + + chain.push_action(config::system_account_name, updateauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("permission", "first") + ("parent", "active") + ("auth", authority(chain.get_public_key(tester_account, "first"), 10)) + ); + chain.push_action(config::system_account_name, linkauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("code", eosio_token) + ("type", "transfer") + ("requirement", "first")); + chain.produce_blocks(); + chain.push_action("eosio.token"_n, "create"_n, "eosio.token"_n, mutable_variant_object() + ("issuer", eosio_token) + ("maximum_supply", "9000000.0000 CUR") + ); + chain.push_action("eosio.token"_n, name("issue"), "eosio.token"_n, fc::mutable_variant_object() + ("to", eosio_token) + ("quantity", "1000000.0000 CUR") + ("memo", "for stuff") + ); + + auto trace = chain.push_action("eosio.token"_n, name("transfer"), "eosio.token"_n, fc::mutable_variant_object() + ("from", eosio_token) + ("to", "tester") + ("quantity", "100.0000 CUR") + ("memo", "hi" ) + ); + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); auto gen_size = chain.control->db().get_index().size(); - BOOST_CHECK_EQUAL(0u, gen_size); + BOOST_REQUIRE_EQUAL(0u, gen_size); - chain.push_action( contract_account, "delayedcall"_n, test_account, fc::mutable_variant_object() - ("payer", test_account) - ("sender_id", 1) - ("contract", contract_account) - ("payload", 42) - ("delay_sec", 1000) - ("replace_existing", false) + chain.produce_blocks(); + auto liquid_balance = get_currency_balance(chain, "eosio.token"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("999900.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + + // this transaction will be delayed 20 blocks + trace = chain.push_action("eosio.token"_n, name("transfer"), "tester"_n, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "1.0000 CUR") + ("memo", "hi" ), + 30, 10 ); + //wdump((fc::json::to_pretty_string(trace))); + ids.push_back(trace->id); + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(1u, gen_size); + BOOST_CHECK_EQUAL(0u, trace->action_traces.size()); const auto& idx = chain.control->db().get_index(); - gen_size = idx.size(); - BOOST_CHECK_EQUAL(1u, gen_size); - auto deferred_id = idx.begin()->trx_id; + auto itr = idx.find( trace->id ); + BOOST_CHECK_EQUAL( (itr != idx.end()), true ); - // canceldelay assumes sender and sender_id to be a specific - // format. hardcode them for testing purpose only - modify_gto_for_canceldelay_test(*(chain.control.get()), deferred_id); + chain.produce_blocks(); - // send canceldelay for the delayed transaction - signed_transaction trx; - trx.actions.emplace_back( - vector{{contract_account, config::active_name}}, - chain::canceldelay{{contract_account, config::active_name}, deferred_id} + liquid_balance = get_currency_balance(chain, "eosio.token"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("999900.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + BOOST_REQUIRE_EXCEPTION( + chain.push_action( config::system_account_name, + updateauth::get_name(), + vector{{tester_account, "first"_n}}, + fc::mutable_variant_object() + ("account", "tester") + ("permission", "first") + ("parent", "active") + ("auth", authority(chain.get_public_key(tester_account, "first"))), + 30, 7 + ), + unsatisfied_authorization, + fc_exception_message_starts_with("transaction declares authority") + ); + + // this transaction will be delayed 20 blocks + trace = chain.push_action(config::system_account_name, updateauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("permission", "first") + ("parent", "active") + ("auth", authority(chain.get_public_key(tester_account, "first"))), + 30, 10 ); + //wdump((fc::json::to_pretty_string(trace))); + ids.push_back(trace->id); + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(2u, gen_size); + BOOST_CHECK_EQUAL(0u, trace->action_traces.size()); + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + chain.produce_blocks(16); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // this transaction will be delayed 20 blocks + trace = chain.push_action("eosio.token"_n, name("transfer"), "tester"_n, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "5.0000 CUR") + ("memo", "hi" ), + 30, 10 + ); + //wdump((fc::json::to_pretty_string(trace))); + ids.push_back(trace->id); + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(3u, gen_size); + BOOST_CHECK_EQUAL(0u, trace->action_traces.size()); + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // send canceldelay for first delayed transaction + signed_transaction trx; + trx.actions.emplace_back(vector{{"tester"_n, config::active_name}}, + chain::canceldelay{{"tester"_n, config::active_name}, ids[0]}); + chain.set_transaction_headers(trx); - trx.sign(chain.get_private_key(contract_account, "active"), chain.control->get_chain_id()); + trx.sign(chain.get_private_key("tester"_n, "active"), chain.control->get_chain_id()); + // first push as a dry_run trx + trace = chain.push_transaction(trx, fc::time_point::maximum(), base_tester::DEFAULT_BILLED_CPU_TIME_US, false, transaction_metadata::trx_type::dry_run); + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); + // now push for real + trace = chain.push_transaction(trx); + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(2u, gen_size); + + const auto& cidx = chain.control->db().get_index(); + auto citr = cidx.find( ids[0] ); + BOOST_CHECK_EQUAL( (citr == cidx.end()), true ); + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(2u, gen_size); + + chain.produce_blocks(); + + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(2u, gen_size); - chain.push_transaction(trx); + chain.produce_blocks(); + // update auth will finally be performed + + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(1u, gen_size); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // this transfer is performed right away since delay is removed + trace = chain.push_action("eosio.token"_n, name("transfer"), "tester"_n, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "10.0000 CUR") + ("memo", "hi" ) + ); + //wdump((fc::json::to_pretty_string(trace))); + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); + + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(1u, gen_size); + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("90.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("10.0000 CUR"), liquid_balance); + + chain.produce_blocks(15); + + gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(1u, gen_size); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("90.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("10.0000 CUR"), liquid_balance); + + // second transfer finally is performed + chain.produce_blocks(); gen_size = chain.control->db().get_index().size(); BOOST_CHECK_EQUAL(0u, gen_size); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("85.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("15.0000 CUR"), liquid_balance); } FC_LOG_AND_RETHROW() } /// canceldelay_test // test canceldelay action under different permission levels @@ -1442,77 +1596,265 @@ BOOST_AUTO_TEST_CASE( canceldelay_test2 ) { try { validating_tester_no_disable_deferred_trx chain; chain.produce_block(); - const auto& contract_account = account_name("defcontract"); - const auto& tester_account = account_name("tester"); + const auto& tester_account = "tester"_n; chain.produce_blocks(); - chain.create_accounts({contract_account, tester_account}); + chain.create_account("eosio.token"_n); chain.produce_blocks(); - chain.set_code(contract_account, test_contracts::deferred_test_wasm()); - chain.set_abi(contract_account, test_contracts::deferred_test_abi()); + + chain.set_code("eosio.token"_n, test_contracts::eosio_token_wasm()); + chain.set_abi("eosio.token"_n, test_contracts::eosio_token_abi()); + + chain.produce_blocks(); + chain.create_account("tester"_n); + chain.create_account("tester2"_n); chain.produce_blocks(); - chain.push_action(config::system_account_name, updateauth::get_name(), contract_account, fc::mutable_variant_object() - ("account", "defcontract") + chain.push_action(config::system_account_name, updateauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") ("permission", "first") ("parent", "active") - ("auth", authority(chain.get_public_key(contract_account, "first"), 5)) + ("auth", authority(chain.get_public_key(tester_account, "first"), 5)) + ); + chain.push_action(config::system_account_name, updateauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("permission", "second") + ("parent", "first") + ("auth", authority(chain.get_public_key(tester_account, "second"))) ); + chain.push_action(config::system_account_name, linkauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("code", eosio_token) + ("type", "transfer") + ("requirement", "first")); + chain.produce_blocks(); + chain.push_action("eosio.token"_n, "create"_n, "eosio.token"_n, mutable_variant_object() + ("issuer", eosio_token) + ("maximum_supply", "9000000.0000 CUR") + ); - auto gen_size = chain.control->db().get_index().size(); - BOOST_CHECK_EQUAL(0u, gen_size); + chain.push_action("eosio.token"_n, name("issue"), "eosio.token"_n, fc::mutable_variant_object() + ("to", eosio_token) + ("quantity", "1000000.0000 CUR") + ("memo", "for stuff") + ); - chain.push_action( contract_account, "delayedcall"_n, tester_account, fc::mutable_variant_object() - ("payer", tester_account) - ("sender_id", 1) - ("contract", contract_account) - ("payload", 42) - ("delay_sec", 1000) - ("replace_existing", false) + auto trace = chain.push_action("eosio.token"_n, name("transfer"), "eosio.token"_n, fc::mutable_variant_object() + ("from", eosio_token) + ("to", "tester") + ("quantity", "100.0000 CUR") + ("memo", "hi" ) ); + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); + auto gen_size = chain.control->db().get_index().size(); + BOOST_REQUIRE_EQUAL(0u, gen_size); - const auto& idx = chain.control->db().get_index(); - gen_size = idx.size(); - BOOST_CHECK_EQUAL(1u, gen_size); - auto deferred_id = idx.begin()->trx_id; + chain.produce_blocks(); + auto liquid_balance = get_currency_balance(chain, "eosio.token"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("999900.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); - // canceldelay assumes sender and sender_id to be a specific - // format. hardcode them for testing purpose only - modify_gto_for_canceldelay_test(*(chain.control.get()), deferred_id); + ilog("attempting first delayed transfer"); - // attempt canceldelay with wrong canceling_auth for delayed trx { + // this transaction will be delayed 10 blocks + trace = chain.push_action("eosio.token"_n, name("transfer"), vector{{"tester"_n, "first"_n}}, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "1.0000 CUR") + ("memo", "hi" ), + 30, 5 + ); + auto trx_id = trace->id; + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_REQUIRE_EQUAL(1u, gen_size); + BOOST_REQUIRE_EQUAL(0u, trace->action_traces.size()); + + const auto& idx = chain.control->db().get_index(); + auto itr = idx.find( trx_id ); + BOOST_CHECK_EQUAL( (itr != idx.end()), true ); + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // attempt canceldelay with wrong canceling_auth for delayed transfer of 1.0000 CUR + { + signed_transaction trx; + trx.actions.emplace_back(vector{{"tester"_n, config::active_name}}, + chain::canceldelay{{"tester"_n, config::active_name}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key("tester"_n, "active"), chain.control->get_chain_id()); + BOOST_REQUIRE_EXCEPTION( chain.push_transaction(trx), action_validate_exception, + fc_exception_message_is("canceling_auth in canceldelay action was not found as authorization in the original delayed transaction") ); + } + + // attempt canceldelay with "second" permission for delayed transfer of 1.0000 CUR + { + signed_transaction trx; + trx.actions.emplace_back(vector{{"tester"_n, "second"_n}}, + chain::canceldelay{{"tester"_n, "first"_n}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key("tester"_n, "second"), chain.control->get_chain_id()); + BOOST_REQUIRE_THROW( chain.push_transaction(trx), irrelevant_auth_exception ); + BOOST_REQUIRE_EXCEPTION( chain.push_transaction(trx), irrelevant_auth_exception, + fc_exception_message_starts_with("canceldelay action declares irrelevant authority") ); + } + + // canceldelay with "active" permission for delayed transfer of 1.0000 CUR signed_transaction trx; trx.actions.emplace_back(vector{{"tester"_n, config::active_name}}, - chain::canceldelay{{"tester"_n, config::active_name}, deferred_id}); + chain::canceldelay{{"tester"_n, "first"_n}, trx_id}); chain.set_transaction_headers(trx); trx.sign(chain.get_private_key("tester"_n, "active"), chain.control->get_chain_id()); - BOOST_REQUIRE_EXCEPTION( chain.push_transaction(trx), action_validate_exception, - fc_exception_message_is("canceling_auth in canceldelay action was not found as authorization in the original delayed transaction") ); + trace = chain.push_transaction(trx); + + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_REQUIRE_EQUAL(0u, gen_size); + + const auto& cidx = chain.control->db().get_index(); + auto citr = cidx.find( trx_id ); + BOOST_REQUIRE_EQUAL( (citr == cidx.end()), true ); + + chain.produce_blocks(10); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); } - // attempt canceldelay with wrong permission for delayed trx + ilog("reset minimum permission of transfer to second permission"); + + chain.push_action(config::system_account_name, linkauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("code", eosio_token) + ("type", "transfer") + ("requirement", "second"), + 30, 5 + ); + + chain.produce_blocks(11); + + + ilog("attempting second delayed transfer"); { + // this transaction will be delayed 10 blocks + trace = chain.push_action("eosio.token"_n, name("transfer"), vector{{"tester"_n, "second"_n}}, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "5.0000 CUR") + ("memo", "hi" ), + 30, 5 + ); + auto trx_id = trace->id; + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace->receipt->status); + auto gen_size = chain.control->db().get_index().size(); + BOOST_CHECK_EQUAL(1u, gen_size); + BOOST_CHECK_EQUAL(0u, trace->action_traces.size()); + + const auto& idx = chain.control->db().get_index(); + auto itr = idx.find( trx_id ); + BOOST_CHECK_EQUAL( (itr != idx.end()), true ); + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // canceldelay with "first" permission for delayed transfer of 5.0000 CUR signed_transaction trx; - trx.actions.emplace_back(vector{{contract_account, "first"_n}}, - chain::canceldelay{{contract_account, "first"_n}, deferred_id}); + trx.actions.emplace_back(vector{{"tester"_n, "first"_n}}, + chain::canceldelay{{"tester"_n, "second"_n}, trx_id}); chain.set_transaction_headers(trx); - trx.sign(chain.get_private_key(contract_account, "first"), chain.control->get_chain_id()); - BOOST_REQUIRE_EXCEPTION( chain.push_transaction(trx), action_validate_exception, - fc_exception_message_is("canceling_auth in canceldelay action was not found as authorization in the original delayed transaction") ); + trx.sign(chain.get_private_key("tester"_n, "first"), chain.control->get_chain_id()); + trace = chain.push_transaction(trx); + + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_REQUIRE_EQUAL(0u, gen_size); + + const auto& cidx = chain.control->db().get_index(); + auto citr = cidx.find( trx_id ); + BOOST_REQUIRE_EQUAL( (citr == cidx.end()), true ); + + chain.produce_blocks(10); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); } - // attempt canceldelay with wrong signature for delayed trx + ilog("attempting third delayed transfer"); + { + // this transaction will be delayed 10 blocks + trace = chain.push_action("eosio.token"_n, name("transfer"), vector{{"tester"_n, config::owner_name}}, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "10.0000 CUR") + ("memo", "hi" ), + 30, 5 + ); + auto trx_id = trace->id; + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_REQUIRE_EQUAL(1u, gen_size); + BOOST_REQUIRE_EQUAL(0u, trace->action_traces.size()); + + const auto& idx = chain.control->db().get_index(); + auto itr = idx.find( trx_id ); + BOOST_CHECK_EQUAL( (itr != idx.end()), true ); + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // attempt canceldelay with "active" permission for delayed transfer of 10.0000 CUR + { + signed_transaction trx; + trx.actions.emplace_back(vector{{"tester"_n, "active"_n}}, + chain::canceldelay{{"tester"_n, config::owner_name}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key("tester"_n, "active"), chain.control->get_chain_id()); + BOOST_REQUIRE_THROW( chain.push_transaction(trx), irrelevant_auth_exception ); + } + + // canceldelay with "owner" permission for delayed transfer of 10.0000 CUR signed_transaction trx; - trx.actions.emplace_back(vector{{contract_account, config::active_name}}, - chain::canceldelay{{contract_account, config::active_name}, deferred_id}); + trx.actions.emplace_back(vector{{"tester"_n, config::owner_name}}, + chain::canceldelay{{"tester"_n, config::owner_name}, trx_id}); chain.set_transaction_headers(trx); - trx.sign(chain.get_private_key(contract_account, "first"), chain.control->get_chain_id()); - BOOST_REQUIRE_THROW( chain.push_transaction(trx), unsatisfied_authorization ); - BOOST_REQUIRE_EXCEPTION( chain.push_transaction(trx), unsatisfied_authorization, - fc_exception_message_starts_with("transaction declares authority") ); + trx.sign(chain.get_private_key("tester"_n, "owner"), chain.control->get_chain_id()); + trace = chain.push_transaction(trx); + + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace->receipt->status); + gen_size = chain.control->db().get_index().size(); + BOOST_REQUIRE_EQUAL(0u, gen_size); + + const auto& cidx = chain.control->db().get_index(); + auto citr = cidx.find( trx_id ); + BOOST_REQUIRE_EQUAL( (citr == cidx.end()), true ); + + chain.produce_blocks(10); + + liquid_balance = get_currency_balance(chain, "tester"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, "tester2"_n); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); } } FC_LOG_AND_RETHROW() } /// canceldelay_test2 diff --git a/unittests/protocol_feature_tests.cpp b/unittests/protocol_feature_tests.cpp index 86cf9ad719..ebef30e756 100644 --- a/unittests/protocol_feature_tests.cpp +++ b/unittests/protocol_feature_tests.cpp @@ -1921,18 +1921,15 @@ BOOST_AUTO_TEST_CASE( set_parameters_packed_test ) { try { c.error("alice does not have permission to call this API")); } FC_LOG_AND_RETHROW() } -// native action hardcodes sender empty and builds sender_id from trx id. -// modify_gto_for_canceldelay_test modifies those two fields for contract -// generated deferred trxs so canceldelay can be used. defined in delay_tests.cpp -namespace eosio::chain { extern void modify_gto_for_canceldelay_test(controller& control, const transaction_id_type& trx_id) ; } - BOOST_AUTO_TEST_CASE( disable_deferred_trxs_stage_1_no_op_test ) { try { tester_no_disable_deferred_trx c; c.produce_block(); - c.create_accounts( {"alice"_n, "bob"_n, "test"_n} ); + c.create_accounts( {"alice"_n, "bob"_n, "test"_n, "payloadless"_n} ); c.set_code( "test"_n, test_contracts::deferred_test_wasm() ); c.set_abi( "test"_n, test_contracts::deferred_test_abi() ); + c.set_code( "payloadless"_n, test_contracts::payloadless_wasm() ); + c.set_abi( "payloadless"_n, test_contracts::payloadless_abi().data() ); c.produce_block(); auto gen_size = c.control->db().get_index().size(); @@ -1959,7 +1956,7 @@ BOOST_AUTO_TEST_CASE( disable_deferred_trxs_stage_1_no_op_test ) { try { gen_size = c.control->db().get_index().size(); BOOST_REQUIRE_EQUAL(0u, gen_size); - // generate a new deferred trx for the rest of the test + // generate a deferred trx from contract for cancel_deferred test c.push_action( "test"_n, "delayedcall"_n, "alice"_n, fc::mutable_variant_object() ("payer", "alice") ("sender_id", 1) @@ -1968,12 +1965,25 @@ BOOST_AUTO_TEST_CASE( disable_deferred_trxs_stage_1_no_op_test ) { try { ("delay_sec", 120) ("replace_existing", false) ); + + // generate a delayed trx for canceldelay test + constexpr uint32_t delay_sec = 10; + c.push_action("payloadless"_n, "doit"_n, "payloadless"_n, mutable_variant_object(), c.DEFAULT_EXPIRATION_DELTA, delay_sec); + + // make sure two trxs were generated c.produce_block(); const auto& idx = c.control->db().get_index(); gen_size = idx.size(); - BOOST_REQUIRE_EQUAL(1u, gen_size); - BOOST_REQUIRE_EQUAL(idx.begin()->payer, "alice"_n); - auto alice_trx_id = idx.begin()->trx_id; + BOOST_REQUIRE_EQUAL(2u, gen_size); + transaction_id_type alice_trx_id; + transaction_id_type payloadless_trx_id; + for( auto itr = idx.begin(); itr != idx.end(); ++itr ) { + if( itr->payer == "alice"_n) { + alice_trx_id = itr->trx_id; + } else { + payloadless_trx_id = itr->trx_id; + } + } // activate disable_deferred_trxs_stage_1 const auto& pfm = c.control->get_protocol_feature_manager(); @@ -1995,7 +2005,7 @@ BOOST_AUTO_TEST_CASE( disable_deferred_trxs_stage_1_no_op_test ) { try { // verify bob's deferred trx is not made to generated_transaction_multi_index gen_size = c.control->db().get_index().size(); - BOOST_REQUIRE_EQUAL(1u, gen_size); + BOOST_REQUIRE_EQUAL(2u, gen_size); // verify alice's deferred trx is still in generated_transaction_multi_index auto gto = c.control->db().find(alice_trx_id); BOOST_REQUIRE(gto != nullptr); @@ -2007,31 +2017,27 @@ BOOST_AUTO_TEST_CASE( disable_deferred_trxs_stage_1_no_op_test ) { try { eosio_assert_message_exception, eosio_assert_message_is( "cancel_deferred failed" ) ); gen_size = c.control->db().get_index().size(); - BOOST_REQUIRE_EQUAL(1u, gen_size); + BOOST_REQUIRE_EQUAL(2u, gen_size); // verify alice's deferred trx is not removed gto = c.control->db().find(alice_trx_id); BOOST_REQUIRE( gto ); - // verify canceldelay native action is no-op - - // canceldelay assumes sender and sender_id to be a specific format - modify_gto_for_canceldelay_test(*(c.control.get()), alice_trx_id); // call canceldelay native action signed_transaction trx; trx.actions.emplace_back( - vector{{"test"_n, config::active_name}}, - canceldelay{{"test"_n, config::active_name}, alice_trx_id} + vector{{"payloadless"_n, config::active_name}}, + canceldelay{{"payloadless"_n, config::active_name}, payloadless_trx_id} ); c.set_transaction_headers(trx); - trx.sign(c.get_private_key("test"_n, "active"), c.control->get_chain_id()); + trx.sign(c.get_private_key("payloadless"_n, "active"), c.control->get_chain_id()); c.push_transaction(trx); c.produce_block(); // verify canceldelay is no-op gen_size = c.control->db().get_index().size(); - BOOST_REQUIRE_EQUAL(1u, gen_size); - // verify alice's deferred trx is not removed - gto = c.control->db().find(alice_trx_id); + BOOST_REQUIRE_EQUAL(2u, gen_size); + // verify payloadless' delayed trx is not removed + gto = c.control->db().find(payloadless_trx_id); BOOST_REQUIRE( gto ); } FC_LOG_AND_RETHROW() } /// disable_deferred_trxs_stage_1_no_op_test @@ -2183,4 +2189,86 @@ BOOST_AUTO_TEST_CASE( disable_deferred_trxs_stage_2_dependency_test ) { try { fc_exception_message_starts_with("not all dependencies of protocol feature with digest")); } FC_LOG_AND_RETHROW() } /// disable_deferred_trxs_stage_2_dependency_test +// Verify a block containing delayed transactions is validated +// before DISABLE_DEFERRED_TRXS_STAGE_1 is activated +BOOST_AUTO_TEST_CASE( block_validation_before_stage_1_test ) { try { + tester_no_disable_deferred_trx tester1; + tester_no_disable_deferred_trx tester2; + + tester1.create_accounts( {"payloadless"_n} ); + tester1.set_code( "payloadless"_n, test_contracts::payloadless_wasm() ); + tester1.set_abi( "payloadless"_n, test_contracts::payloadless_abi().data() ); + + // Produce a block containing a delayed trx + constexpr uint32_t delay_sec = 10; + tester1.push_action("payloadless"_n, "doit"_n, "payloadless"_n, mutable_variant_object(), tester1.DEFAULT_EXPIRATION_DELTA, delay_sec); + auto b = tester1.produce_block(); + + // Push the block to another chain. The block should be validated + BOOST_REQUIRE_NO_THROW(tester2.push_block(b)); +} FC_LOG_AND_RETHROW() } /// block_validation_before_stage_1_test + +// Verify a block containing delayed transactions is not validated +// after DISABLE_DEFERRED_TRXS_STAGE_1 is activated +BOOST_AUTO_TEST_CASE( block_validation_after_stage_1_test ) { try { + tester_no_disable_deferred_trx tester1; + + // Activate DISABLE_DEFERRED_TRXS_STAGE_1 such that tester1 + // matches tester2 below + const auto& pfm1 = tester1.control->get_protocol_feature_manager(); + auto d1 = pfm1.get_builtin_digest( builtin_protocol_feature_t::disable_deferred_trxs_stage_1 ); + BOOST_REQUIRE( d1 ); + tester1.preactivate_protocol_features( {*d1} ); + tester1.produce_block(); + + // Create a block with valid transaction + tester1.create_account("newacc"_n); + auto b = tester1.produce_block(); + + // Make a copy of the block + auto copy_b = std::make_shared(std::move(*b)); + // Retrieve the last transaction + auto signed_tx = std::get(copy_b->transactions.back().trx).get_signed_transaction(); + // Make a delayed transaction by forcing delay_sec greater than 0 + signed_tx.delay_sec = 120; + // Re-sign the transaction + signed_tx.signatures.clear(); + signed_tx.sign(tester1.get_private_key(config::system_account_name, "active"), tester1.control->get_chain_id()); + // Replace the original transaction with the delayed transaction + auto delayed_tx = packed_transaction(signed_tx); + copy_b->transactions.back().trx = std::move(delayed_tx); + + // Re-calculate the transaction merkle + deque trx_digests; + const auto& trxs = copy_b->transactions; + for( const auto& a : trxs ) + trx_digests.emplace_back( a.digest() ); + copy_b->transaction_mroot = merkle( std::move(trx_digests) ); + + // Re-sign the block + auto header_bmroot = digest_type::hash( std::make_pair( copy_b->digest(), tester1.control->head_block_state()->blockroot_merkle.get_root() ) ); + auto sig_digest = digest_type::hash( std::make_pair(header_bmroot, tester1.control->head_block_state()->pending_schedule.schedule_hash) ); + copy_b->producer_signature = tester1.get_private_key(config::system_account_name, "active").sign(sig_digest); + + // Create the second chain + tester_no_disable_deferred_trx tester2; + // Activate DISABLE_DEFERRED_TRXS_STAGE_1 on the second chain + const auto& pfm2 = tester2.control->get_protocol_feature_manager(); + auto d2 = pfm2.get_builtin_digest( builtin_protocol_feature_t::disable_deferred_trxs_stage_1 ); + BOOST_REQUIRE( d2 ); + tester2.preactivate_protocol_features( {*d2} ); + tester2.produce_block(); + + // Push the block with delayed transaction to the second chain + auto bsf = tester2.control->create_block_state_future( copy_b->calculate_id(), copy_b ); + tester2.control->abort_block(); + controller::block_report br; + + // The block is invalidated + BOOST_REQUIRE_EXCEPTION(tester2.control->push_block( br, bsf.get(), forked_branch_callback{}, trx_meta_cache_lookup{} ), + fc::exception, + fc_exception_message_starts_with("transaction cannot be delayed") + ); +} FC_LOG_AND_RETHROW() } /// block_validation_after_stage_1_test + BOOST_AUTO_TEST_SUITE_END()