diff --git a/libraries/appbase b/libraries/appbase index 13090992fe..0b2f151bea 160000 --- a/libraries/appbase +++ b/libraries/appbase @@ -1 +1 @@ -Subproject commit 13090992fe675234716fa69d8bd037dbc4755787 +Subproject commit 0b2f151beaceb1340e537bb43766428391ceddc7 diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index f45d145044..77bc55c6a4 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1006,6 +1006,8 @@ struct controller_impl { async_t async_aggregation = async_t::yes; // by default we process incoming votes asynchronously my_finalizers_t my_finalizers; std::atomic writing_snapshot = false; + std::atomic applying_block = false; + platform_timer& main_thread_timer; thread_local static platform_timer timer; // a copy for main thread and each read-only thread #if defined(EOSIO_EOS_VM_RUNTIME_ENABLED) || defined(EOSIO_EOS_VM_JIT_RUNTIME_ENABLED) @@ -1285,6 +1287,7 @@ struct controller_impl { read_mode( cfg.read_mode ), thread_pool(), my_finalizers(cfg.finalizers_dir / config::safety_filename), + main_thread_timer(timer), // assumes constructor is called from main thread wasmif( conf.wasm_runtime, conf.eosvmoc_tierup, db, conf.state_dir, conf.eosvmoc_config, !conf.profile_accounts.empty() ) { assert(cfg.chain_thread_pool_size > 0); @@ -3784,6 +3787,9 @@ struct controller_impl { } } + applying_block = true; + auto apply = fc::make_scoped_exit([&](){ applying_block = false; }); + transaction_trace_ptr trace; size_t packed_idx = 0; @@ -3810,7 +3816,11 @@ struct controller_impl { std::holds_alternative(receipt.trx); if( transaction_failed && !transaction_can_fail) { - edump((*trace)); + if (trace->except->code() == interrupt_exception::code_value) { + ilog("Interrupt of trx id: ${id}", ("id", trace->id)); + } else { + edump((*trace)); + } throw *trace->except; } @@ -3879,7 +3889,8 @@ struct controller_impl { } catch ( const boost::interprocess::bad_alloc& ) { throw; } catch ( const fc::exception& e ) { - edump((e.to_detail_string())); + if (e.code() != interrupt_exception::code_value) + edump((e.to_detail_string())); abort_block(); throw; } catch ( const std::exception& e ) { @@ -4369,7 +4380,15 @@ struct controller_impl { log_irreversible(); transition_to_savanna_if_needed(); return controller::apply_blocks_result::complete; - } FC_LOG_AND_RETHROW( ) + } catch (fc::exception& e) { + if (e.code() != interrupt_exception::code_value) { + wlog("${d}", ("d",e.to_detail_string())); + FC_RETHROW_EXCEPTION(e, warn, "rethrow"); + } + throw; + } catch (...) { + try { throw; } FC_LOG_AND_RETHROW() + } } controller::apply_blocks_result maybe_apply_blocks( const forked_callback_t& forked_cb, const trx_meta_cache_lookup& trx_lookup ) @@ -4441,8 +4460,12 @@ struct controller_impl { } catch ( const boost::interprocess::bad_alloc& ) { throw; } catch (const fc::exception& e) { - elog("exception thrown while applying block ${bn} : ${id}, previous ${p}, error: ${e}", - ("bn", bsp->block_num())("id", bsp->id())("p", bsp->previous())("e", e.to_detail_string())); + if (e.code() == interrupt_exception::code_value) { + ilog("interrupt while applying block ${bn} : ${id}", ("bn", bsp->block_num())("id", bsp->id())); + } else { + elog("exception thrown while applying block ${bn} : ${id}, previous ${p}, error: ${e}", + ("bn", bsp->block_num())("id", bsp->id())("p", bsp->previous())("e", e.to_detail_string())); + } except = std::current_exception(); } catch (const std::exception& e) { elog("exception thrown while applying block ${bn} : ${id}, previous ${p}, error: ${e}", @@ -4505,6 +4528,16 @@ struct controller_impl { return applied_trxs; } + void interrupt_transaction() { + // Only interrupt transaction if applying a block. Speculative trxs already have a deadline set so they + // have limited run time already. This is to allow killing a long-running transaction in a block being + // validated. + if (applying_block) { + ilog("Interrupting apply block"); + main_thread_timer.expire_now(); + } + } + // @param if_active true if instant finality is active static checksum256_type calc_merkle( deque&& digests, bool if_active ) { if (if_active) { @@ -5265,6 +5298,10 @@ deque controller::abort_block() { return my->abort_block(); } +void controller::interrupt_transaction() { + my->interrupt_transaction(); +} + boost::asio::io_context& controller::get_thread_pool() { return my->thread_pool.get_executor(); } diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index cf8f12e7e4..fe58f39f95 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -206,6 +206,9 @@ namespace eosio::chain { */ deque abort_block(); + /// Expected to be called from signal handler + void interrupt_transaction(); + /** * */ diff --git a/libraries/chain/include/eosio/chain/exceptions.hpp b/libraries/chain/include/eosio/chain/exceptions.hpp index 1539b7c3be..fc6ef200b3 100644 --- a/libraries/chain/include/eosio/chain/exceptions.hpp +++ b/libraries/chain/include/eosio/chain/exceptions.hpp @@ -381,6 +381,9 @@ namespace eosio { namespace chain { 3080005, "Transaction CPU usage is too much for the remaining allowable usage of the current block" ) FC_DECLARE_DERIVED_EXCEPTION( deadline_exception, resource_exhausted_exception, 3080006, "Transaction took too long" ) + FC_DECLARE_DERIVED_EXCEPTION( leeway_deadline_exception, deadline_exception, + 3081001, "Transaction reached the deadline set due to leeway on account CPU limits" ) + FC_DECLARE_DERIVED_EXCEPTION( greylist_net_usage_exceeded, resource_exhausted_exception, 3080007, "Transaction exceeded the current greylisted account network usage limit" ) FC_DECLARE_DERIVED_EXCEPTION( greylist_cpu_usage_exceeded, resource_exhausted_exception, @@ -389,9 +392,8 @@ namespace eosio { namespace chain { 3080009, "Read-only transaction eos-vm-oc compile temporary failure" ) FC_DECLARE_DERIVED_EXCEPTION( ro_trx_vm_oc_compile_permanent_failure, resource_exhausted_exception, 3080010, "Read-only transaction eos-vm-oc compile permanent failure" ) - - FC_DECLARE_DERIVED_EXCEPTION( leeway_deadline_exception, deadline_exception, - 3081001, "Transaction reached the deadline set due to leeway on account CPU limits" ) + FC_DECLARE_DERIVED_EXCEPTION( interrupt_exception, resource_exhausted_exception, + 3080011, "Transaction interrupted by signal" ) FC_DECLARE_DERIVED_EXCEPTION( authorization_exception, chain_exception, 3090000, "Authorization exception" ) diff --git a/libraries/chain/include/eosio/chain/platform_timer.hpp b/libraries/chain/include/eosio/chain/platform_timer.hpp index 29a8d62d46..72fb8d1fef 100644 --- a/libraries/chain/include/eosio/chain/platform_timer.hpp +++ b/libraries/chain/include/eosio/chain/platform_timer.hpp @@ -17,6 +17,7 @@ struct platform_timer { void start(fc::time_point tp); void stop(); + void expire_now(); /* Sets a callback for when timer expires. Be aware this could might fire from a signal handling context and/or on any particular thread. Only a single callback can be registered at once; trying to register more will diff --git a/libraries/chain/platform_timer_asio_fallback.cpp b/libraries/chain/platform_timer_asio_fallback.cpp index 28525c7968..547bc5dcc7 100644 --- a/libraries/chain/platform_timer_asio_fallback.cpp +++ b/libraries/chain/platform_timer_asio_fallback.cpp @@ -57,39 +57,36 @@ platform_timer::~platform_timer() { void platform_timer::start(fc::time_point tp) { if(tp == fc::time_point::maximum()) { - expired = 0; + expired = false; return; } fc::microseconds x = tp.time_since_epoch() - fc::time_point::now().time_since_epoch(); if(x.count() <= 0) - expired = 1; + expired = true; else { -#if 0 - std::promise p; - auto f = p.get_future(); - checktime_ios->post([&p,this]() { - expired = 0; - p.set_value(); - }); - f.get(); -#endif - expired = 0; + expired = false; my->timer->expires_after(std::chrono::microseconds(x.count())); my->timer->async_wait([this](const boost::system::error_code& ec) { if(ec) return; - expired = 1; - call_expiration_callback(); + expire_now(); }); } } +void platform_timer::expire_now() { + bool expected = false; + if (expired.compare_exchange_strong(expected, true)) { + call_expiration_callback(); + } +} + void platform_timer::stop() { if(expired) return; my->timer->cancel(); - expired = 1; + expired = true; } }} diff --git a/libraries/chain/platform_timer_kqueue.cpp b/libraries/chain/platform_timer_kqueue.cpp index 3cb341a031..16f076a0cb 100644 --- a/libraries/chain/platform_timer_kqueue.cpp +++ b/libraries/chain/platform_timer_kqueue.cpp @@ -58,8 +58,7 @@ platform_timer::platform_timer() { if(c == 1 && anEvent.filter == EVFILT_TIMER) { platform_timer* self = (platform_timer*)anEvent.udata; - self->expired = 1; - self->call_expiration_callback(); + self->expire_now(); } else if(c == 1 && anEvent.filter == EVFILT_USER) return; @@ -90,19 +89,26 @@ platform_timer::~platform_timer() { void platform_timer::start(fc::time_point tp) { if(tp == fc::time_point::maximum()) { - expired = 0; + expired = false; return; } fc::microseconds x = tp.time_since_epoch() - fc::time_point::now().time_since_epoch(); if(x.count() <= 0) - expired = 1; + expired = true; else { struct kevent64_s aTimerEvent; EV_SET64(&aTimerEvent, my->timerid, EVFILT_TIMER, EV_ADD|EV_ENABLE|EV_ONESHOT, NOTE_USECONDS|NOTE_CRITICAL, x.count(), (uint64_t)this, 0, 0); - expired = 0; + expired = false; if(kevent64(kqueue_fd, &aTimerEvent, 1, NULL, 0, KEVENT_FLAG_IMMEDIATE, NULL) != 0) - expired = 1; + expired = true; + } +} + +void platform_timer::expire_now() { + bool expected = false; + if (expired.compare_exchange_strong(expected, true)) { + call_expiration_callback(); } } @@ -113,7 +119,7 @@ void platform_timer::stop() { struct kevent64_s stop_timer_event; EV_SET64(&stop_timer_event, my->timerid, EVFILT_TIMER, EV_DELETE, 0, 0, 0, 0, 0); kevent64(kqueue_fd, &stop_timer_event, 1, NULL, 0, KEVENT_FLAG_IMMEDIATE, NULL); - expired = 1; + expired = true; } }} diff --git a/libraries/chain/platform_timer_posix.cpp b/libraries/chain/platform_timer_posix.cpp index bb000de5c3..4388fa18b9 100644 --- a/libraries/chain/platform_timer_posix.cpp +++ b/libraries/chain/platform_timer_posix.cpp @@ -5,12 +5,14 @@ #include #include +#include #include #include #include +#include -namespace eosio { namespace chain { +namespace eosio::chain { static_assert(std::atomic_bool::is_always_lock_free, "Only lock-free atomics AS-safe."); @@ -19,8 +21,7 @@ struct platform_timer::impl { static void sig_handler(int, siginfo_t* si, void*) { platform_timer* self = (platform_timer*)si->si_value.sival_ptr; - self->expired = 1; - self->call_expiration_callback(); + self->expire_now(); } }; @@ -28,9 +29,9 @@ platform_timer::platform_timer() { static_assert(sizeof(impl) <= fwd_size); static bool initialized; - static std::mutex initalized_mutex; + static std::mutex initialized_mutex; - if(std::lock_guard guard(initalized_mutex); !initialized) { + if(std::lock_guard guard(initialized_mutex); !initialized) { struct sigaction act; sigemptyset(&act.sa_mask); act.sa_sigaction = impl::sig_handler; @@ -55,19 +56,26 @@ platform_timer::~platform_timer() { void platform_timer::start(fc::time_point tp) { if(tp == fc::time_point::maximum()) { - expired = 0; + expired = false; return; } fc::microseconds x = tp.time_since_epoch() - fc::time_point::now().time_since_epoch(); if(x.count() <= 0) - expired = 1; + expired = true; else { time_t secs = x.count() / 1000000; long nsec = (x.count() - (secs*1000000)) * 1000; struct itimerspec enable = {{0, 0}, {secs, nsec}}; - expired = 0; + expired = false; if(timer_settime(my->timerid, 0, &enable, NULL) != 0) - expired = 1; + expired = true; + } +} + +void platform_timer::expire_now() { + bool expected = false; + if (expired.compare_exchange_strong(expected, true)) { + call_expiration_callback(); } } @@ -76,7 +84,7 @@ void platform_timer::stop() { return; struct itimerspec disable = {{0, 0}, {0, 0}}; timer_settime(my->timerid, 0, &disable, NULL); - expired = 1; + expired = true; } -}} +} diff --git a/libraries/chain/transaction_context.cpp b/libraries/chain/transaction_context.cpp index c59a9f2f8b..420b22ebec 100644 --- a/libraries/chain/transaction_context.cpp +++ b/libraries/chain/transaction_context.cpp @@ -459,7 +459,10 @@ namespace eosio::chain { return; auto now = fc::time_point::now(); - if( explicit_billed_cpu_time || deadline_exception_code == deadline_exception::code_value ) { + if (explicit_billed_cpu_time && block_deadline > now) { + EOS_THROW( interrupt_exception, "interrupt signaled, ran ${bt}us, start ${s}", + ("bt", now - pseudo_start)("s", start) ); + } else if( explicit_billed_cpu_time || deadline_exception_code == deadline_exception::code_value ) { EOS_THROW( deadline_exception, "deadline exceeded ${billing_timer}us", ("billing_timer", now - pseudo_start)("now", now)("deadline", _deadline)("start", start) ); } else if( deadline_exception_code == block_cpu_usage_exceeded::code_value ) { diff --git a/plugins/test_control_api_plugin/test_control_api_plugin.cpp b/plugins/test_control_api_plugin/test_control_api_plugin.cpp index e2b5a6892a..ce13bd6408 100644 --- a/plugins/test_control_api_plugin/test_control_api_plugin.cpp +++ b/plugins/test_control_api_plugin/test_control_api_plugin.cpp @@ -51,6 +51,10 @@ void test_control_api_plugin::plugin_startup() { TEST_CONTROL_RW_CALL(throw_on, 202, http_params_types::params_required) }, appbase::exec_queue::read_write); + app().get_plugin().add_api({ + TEST_CONTROL_RW_CALL(swap_action, 202, http_params_types::params_required) + }, appbase::exec_queue::read_write); + } void test_control_api_plugin::plugin_shutdown() {} diff --git a/plugins/test_control_plugin/include/eosio/test_control_plugin/test_control_plugin.hpp b/plugins/test_control_plugin/include/eosio/test_control_plugin/test_control_plugin.hpp index 970e711673..3f772f4bc2 100644 --- a/plugins/test_control_plugin/include/eosio/test_control_plugin/test_control_plugin.hpp +++ b/plugins/test_control_plugin/include/eosio/test_control_plugin/test_control_plugin.hpp @@ -33,7 +33,17 @@ class read_write { }; empty throw_on(const throw_on_params& params) const; - private: + // produce a next block with `from` action replaced with `to` action + // requires Savanna to be active, this assumes blocks are is_proper_svnn_block + struct swap_action_params { + chain::name from; // replace from action in block to `to` action + chain::name to; + fc::crypto::private_key trx_priv_key; + fc::crypto::private_key blk_priv_key; + }; + empty swap_action(const swap_action_params& params) const; + +private: test_control_ptr my; }; @@ -68,3 +78,4 @@ class test_control_plugin : public plugin { FC_REFLECT(eosio::test_control_apis::empty, ) FC_REFLECT(eosio::test_control_apis::read_write::kill_node_on_producer_params, (producer)(where_in_sequence)(based_on_lib) ) FC_REFLECT(eosio::test_control_apis::read_write::throw_on_params, (signal)(exception) ) +FC_REFLECT(eosio::test_control_apis::read_write::swap_action_params, (from)(to)(trx_priv_key)(blk_priv_key) ) diff --git a/plugins/test_control_plugin/test_control_plugin.cpp b/plugins/test_control_plugin/test_control_plugin.cpp index 8402623e30..8644e56fa9 100644 --- a/plugins/test_control_plugin/test_control_plugin.cpp +++ b/plugins/test_control_plugin/test_control_plugin.cpp @@ -12,10 +12,11 @@ class test_control_plugin_impl { void kill_on_head(account_name prod, uint32_t where_in_seq); void set_throw_on_options(const test_control_apis::read_write::throw_on_params& throw_options); + void set_swap_action_options(const test_control_apis::read_write::swap_action_params& swap_options); private: void block_start(chain::block_num_type block_num); void accepted_block_header(const chain::block_id_type& id); - void accepted_block(const chain::block_id_type& id); + void accepted_block(const chain::block_id_type& id, const chain::signed_block_ptr& block); void irreversible_block(const chain::block_id_type& id); void applied_transaction(); void voted_block(); @@ -25,6 +26,9 @@ class test_control_plugin_impl { void reset_throw(); void process_next_block_state(const chain::block_id_type& id); + void swap_action_in_block(const chain::signed_block_ptr& b); + void reset_swap_action() { _swap_on_options = {}; } + chain::controller& _chain; struct kill_options { account_name _producer; @@ -35,7 +39,8 @@ class test_control_plugin_impl { bool _track_head{false}; } _kill_options; - test_control_apis::read_write::throw_on_params _throw_options; + test_control_apis::read_write::throw_on_params _throw_options; + test_control_apis::read_write::swap_action_params _swap_on_options; std::optional _block_start_connection; std::optional _accepted_block_header_connection; @@ -59,7 +64,7 @@ void test_control_plugin_impl::connect() { _accepted_block_connection = _chain.accepted_block().connect( [&]( const chain::block_signal_params& t ) { const auto& [ block, id ] = t; - accepted_block( id ); + accepted_block( id, block ); } ); _irreversible_block_connection.emplace( _chain.irreversible_block().connect( [&]( const chain::block_signal_params& t ) { @@ -96,6 +101,71 @@ void test_control_plugin_impl::reset_throw() { _throw_options = test_control_apis::read_write::throw_on_params{}; } +void test_control_plugin_impl::swap_action_in_block(const chain::signed_block_ptr& b) { + if (b->transactions.empty()) + return; + + bool found = std::find_if(b->transactions.cbegin(), b->transactions.cend(), [&](const auto& t) { + return std::visit(chain::overloaded{ + [](const transaction_id_type&) { return false; }, + [&](const chain::packed_transaction& pt) { + for (const auto& a : pt.get_transaction().actions) { + if (a.name == _swap_on_options.from) + return true; + } + return false; + } + }, t.trx); + }) != b->transactions.cend(); + if (!found) + return; + + if (!b->is_proper_svnn_block()) { + elog("Block is not a Savanna block, swap_action failed."); + return; + } + + auto copy_b = std::make_shared(b->clone()); + copy_b->previous = b->calculate_id(); + copy_b->block_extensions.clear(); // remove QC extension since header will claim same as previous block + copy_b->timestamp = b->timestamp.next(); + // swap out action + for (auto& t : copy_b->transactions) { + std::visit(chain::overloaded{ + [](const transaction_id_type&) {}, + [&](chain::packed_transaction& pt) { + for (auto& a : pt.get_transaction().actions) { + if (a.name == _swap_on_options.from) { + auto signed_tx = pt.get_signed_transaction(); + auto& act = signed_tx.actions.back(); + act.name = _swap_on_options.to; + // Re-sign the transaction + signed_tx.signatures.clear(); + signed_tx.sign(_swap_on_options.trx_priv_key, _chain.get_chain_id()); + // Replace the transaction + auto new_packed_tx = packed_transaction(signed_tx); + const_cast(pt) = std::move(new_packed_tx); + } + } + } + }, t.trx); + } + // Re-calculate the transaction merkle + std::deque trx_digests; + const auto& trxs = copy_b->transactions; + for( const auto& tr : trxs ) + trx_digests.emplace_back( tr.digest() ); + copy_b->transaction_mroot = chain::calculate_merkle( std::move(trx_digests) ); + // Re-sign the block + copy_b->producer_signature = _swap_on_options.blk_priv_key.sign(copy_b->calculate_id()); + + // will be processed on the next start_block if is_new_best_head + const auto&[is_new_best_head, bh] = _chain.accept_block(copy_b->calculate_id(), copy_b); + ilog("Swapped action ${f} to ${t}, is_new_best_head ${bh}, block ${bn}", + ("f", _swap_on_options.from)("t", _swap_on_options.to)("bh", is_new_best_head)("bn", bh ? bh->block_num() : 0)); + reset_swap_action(); +} + void test_control_plugin_impl::block_start(chain::block_num_type block_num) { if (_throw_options.signal == "block_start") throw_exception(); @@ -106,11 +176,13 @@ void test_control_plugin_impl::accepted_block_header(const chain::block_id_type& throw_exception(); } -void test_control_plugin_impl::accepted_block(const chain::block_id_type& id) { +void test_control_plugin_impl::accepted_block(const chain::block_id_type& id, const chain::signed_block_ptr& block) { if (_kill_options._track_head) process_next_block_state(id); if (_throw_options.signal == "accepted_block") throw_exception(); + if (!_swap_on_options.from.empty()) + swap_action_in_block(block); } void test_control_plugin_impl::irreversible_block(const chain::block_id_type& id) { @@ -185,12 +257,13 @@ void test_control_plugin_impl::kill_on_head(account_name prod, uint32_t where_in _kill_options._track_head = true; } -// ----------------- throw_on -------------------------------- - void test_control_plugin_impl::set_throw_on_options(const test_control_apis::read_write::throw_on_params& throw_options) { _throw_options = throw_options; } +void test_control_plugin_impl::set_swap_action_options(const test_control_apis::read_write::swap_action_params& swap_options) { + _swap_on_options = swap_options; +} test_control_plugin::test_control_plugin() = default; @@ -230,5 +303,11 @@ empty read_write::throw_on(const read_write::throw_on_params& params) const { return {}; } +empty read_write::swap_action(const read_write::swap_action_params& params) const { + ilog("received swap_action: ${p}", ("p", params)); + my->set_swap_action_options(params); + return {}; +} + } // namespace test_control_apis } // namespace eosio diff --git a/programs/nodeos/main.cpp b/programs/nodeos/main.cpp index a73e7cc582..bf161cf79a 100644 --- a/programs/nodeos/main.cpp +++ b/programs/nodeos/main.cpp @@ -168,10 +168,6 @@ int main(int argc, char** argv) uint32_t short_hash = 0; fc::from_hex(eosio::version::version_hash(), (char*)&short_hash, sizeof(short_hash)); - app->set_stop_executor_cb([&app]() { - ilog("appbase quit called"); - app->get_io_context().stop(); - }); app->set_version(htonl(short_hash)); app->set_version_string(eosio::version::version_client()); app->set_full_version_string(eosio::version::version_full()); @@ -192,6 +188,12 @@ int main(int argc, char** argv) } return INITIALIZE_FAIL; } + controller& chain = app->get_plugin().chain(); + app->set_stop_executor_cb([&app, &chain]() { + ilog("appbase quit called"); + chain.interrupt_transaction(); + app->get_io_context().stop(); + }); if (auto resmon_plugin = app->find_plugin()) { resmon_plugin->monitor_directory(app->data_dir()); } else { @@ -220,6 +222,9 @@ int main(int argc, char** argv) elog( "database dirty flag set (likely due to unclean shutdown): replay required" ); return DATABASE_DIRTY; } + } else if (e.code() == interrupt_exception::code_value) { + ilog("Interrupted, successfully exiting"); + return SUCCESS; } elog( "${e}", ("e", e.to_detail_string())); return OTHER_FAIL; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1f833e9f4d..8c77c638de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -90,6 +90,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_contrl_c_test.py ${CMAKE_CURRE configure_file(${CMAKE_CURRENT_SOURCE_DIR}/read_only_trx_test.py ${CMAKE_CURRENT_BINARY_DIR}/read_only_trx_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/resource_monitor_plugin_test.py ${CMAKE_CURRENT_BINARY_DIR}/resource_monitor_plugin_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/light_validation_sync_test.py ${CMAKE_CURRENT_BINARY_DIR}/light_validation_sync_test.py COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/interrupt_trx_test.py ${CMAKE_CURRENT_BINARY_DIR}/interrupt_trx_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/trace_plugin_test.py ${CMAKE_CURRENT_BINARY_DIR}/trace_plugin_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nested_container_multi_index_test.py ${CMAKE_CURRENT_BINARY_DIR}/nested_container_multi_index_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/auto_bp_peering_test.py ${CMAKE_CURRENT_BINARY_DIR}/auto_bp_peering_test.py COPYONLY) @@ -419,6 +420,8 @@ add_test(NAME light_validation_sync_test COMMAND tests/light_validation_sync_tes set_property(TEST light_validation_sync_test PROPERTY LABELS nonparallelizable_tests) add_test(NAME light_validation_sync_if_test COMMAND tests/light_validation_sync_test.py --activate-if -v ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_property(TEST light_validation_sync_if_test PROPERTY LABELS nonparallelizable_tests) +add_test(NAME interrupt_trx_test COMMAND tests/interrupt_trx_test.py -v ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +set_property(TEST interrupt_trx_test PROPERTY LABELS nonparallelizable_tests) add_test(NAME auto_bp_peering_test COMMAND tests/auto_bp_peering_test.py -v ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_property(TEST auto_bp_peering_test PROPERTY LABELS long_running_tests) diff --git a/tests/chain_test_utils.hpp b/tests/chain_test_utils.hpp index 67f4e5f30c..2e4de93272 100644 --- a/tests/chain_test_utils.hpp +++ b/tests/chain_test_utils.hpp @@ -49,6 +49,17 @@ struct reqactivated { } }; +inline private_key_type get_private_key( name keyname, string role ) { + if (keyname == config::system_account_name) + return private_key_type::regenerate(fc::sha256::hash(std::string("nathan"))); + + return private_key_type::regenerate(fc::sha256::hash(keyname.to_string()+role)); +} + +inline public_key_type get_public_key( name keyname, string role ){ + return get_private_key( keyname, role ).get_public_key(); +} + // Create a read-only trx that works with bios reqactivated action inline auto make_bios_ro_trx(eosio::chain::controller& control) { const auto& pfm = control.get_protocol_feature_manager(); @@ -66,12 +77,7 @@ inline auto make_bios_ro_trx(eosio::chain::controller& control) { inline auto push_input_trx(appbase::scoped_app& app, eosio::chain::controller& control, account_name account, signed_transaction& trx) { trx.expiration = fc::time_point_sec{fc::time_point::now() + fc::seconds(30)}; trx.set_reference_block( control.head().id() ); - if (account == config::system_account_name) { - auto default_priv_key = private_key_type::regenerate(fc::sha256::hash(std::string("nathan"))); - trx.sign(default_priv_key, control.get_chain_id()); - } else { - trx.sign(testing::tester::get_private_key(account, "active"), control.get_chain_id()); - } + trx.sign(get_private_key(account, "active"), control.get_chain_id()); auto ptrx = std::make_shared( trx, packed_transaction::compression_type::zlib ); auto trx_promise = std::make_shared>(); @@ -120,6 +126,23 @@ inline auto set_code(appbase::scoped_app& app, eosio::chain::controller& control return push_input_trx(app, control, account, trx); } +inline transaction_trace_ptr create_account(appbase::scoped_app& app, eosio::chain::controller& control, account_name a, account_name creator) { + signed_transaction trx; + + authority owner_auth{ get_public_key( a, "owner" ) }; + authority active_auth{ get_public_key( a, "active" ) }; + + trx.actions.emplace_back( vector{{creator,config::active_name}}, + chain::newaccount{ + .creator = creator, + .name = a, + .owner = owner_auth, + .active = active_auth, + }); + + return push_input_trx(app, control, creator, trx); +} + inline void activate_protocol_features_set_bios_contract(appbase::scoped_app& app, chain_plugin* chain_plug) { using namespace appbase; diff --git a/tests/interrupt_trx_test.py b/tests/interrupt_trx_test.py new file mode 100755 index 0000000000..b39172f4c1 --- /dev/null +++ b/tests/interrupt_trx_test.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 + +import json +import signal + +from TestHarness import Account, Cluster, Node, ReturnType, TestHelper, Utils, WalletMgr +from TestHarness.TestHelper import AppArgs + +############################################################### +# interrupt_trx_test +# +# Test applying a block with an infinite trx and verify SIGTERM kill +# interrupts the transaction and aborts the block. +# +############################################################### + +# Parse command line arguments +args = TestHelper.parse_args({"-v","--dump-error-details","--leave-running","--keep-logs","--unshared"}) +Utils.Debug = args.v +dumpErrorDetails=args.dump_error_details +dontKill=args.leave_running +keepLogs=args.keep_logs + +EOSIO_ACCT_PRIVATE_DEFAULT_KEY = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +EOSIO_ACCT_PUBLIC_DEFAULT_KEY = "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + +walletMgr=WalletMgr(True) +cluster=Cluster(unshared=args.unshared, keepRunning=args.leave_running, keepLogs=args.keep_logs) +cluster.setWalletMgr(walletMgr) + +testSuccessful = False +try: + TestHelper.printSystemInfo("BEGIN") + assert cluster.launch( + pnodes=1, + prodCount=1, + totalProducers=1, + totalNodes=2, + loadSystemContract=False, + activateIF=True, + extraNodeosArgs="--plugin eosio::test_control_api_plugin") + + prodNode = cluster.getNode(0) + validationNode = cluster.getNode(1) + + # Create a transaction to create an account + Utils.Print("create a new account payloadless from the producer node") + payloadlessAcc = Account("payloadless") + payloadlessAcc.ownerPublicKey = EOSIO_ACCT_PUBLIC_DEFAULT_KEY + payloadlessAcc.activePublicKey = EOSIO_ACCT_PUBLIC_DEFAULT_KEY + prodNode.createAccount(payloadlessAcc, cluster.eosioAccount) + + contractDir="unittests/test-contracts/payloadless" + wasmFile="payloadless.wasm" + abiFile="payloadless.abi" + Utils.Print("Publish payloadless contract") + trans = prodNode.publishContract(payloadlessAcc, contractDir, wasmFile, abiFile, waitForTransBlock=True) + + contract="payloadless" + action="doit" + data="{}" + opts="--permission payloadless@active" + trans=prodNode.pushMessage(contract, action, data, opts) + assert trans and trans[0], "Failed to push doit action" + + action="doitslow" + trans=prodNode.pushMessage(contract, action, data, opts) + assert trans and trans[0], "Failed to push doitslow action" + + action="doitforever" + trans=prodNode.pushMessage(contract, action, data, opts, silentErrors=True) + assert trans and not trans[0], "push doitforever action did not fail as expected" + + prodNode.processUrllibRequest("test_control", "swap_action", {"from": "doitslow", "to": "doitforever"}) + + action="doitslow" + trans=prodNode.pushMessage(contract, action, data, opts) + assert trans and trans[0], "Failed to push doitslow action" + + prodNode.waitForProducer("defproducera") + + prodNode.processUrllibRequest("test_control", "swap_action", + {"from":"doitslow", "to":"doitforever", + "trx_priv_key":EOSIO_ACCT_PRIVATE_DEFAULT_KEY, + "blk_priv_key":cluster.defproduceraAccount.activePrivateKey}) + + assert not prodNode.waitForHeadToAdvance(3), f"prodNode did advance head after doitforever action" + + prodNode.interruptAndVerifyExitStatus() + + assert not prodNode.verifyAlive(), "prodNode did not exit from SIGINT" + + testSuccessful = True +finally: + TestHelper.shutdown(cluster, walletMgr, testSuccessful, dumpErrorDetails) + +exitCode = 0 if testSuccessful else 1 +exit(exitCode) diff --git a/unittests/checktime_tests.cpp b/unittests/checktime_tests.cpp index 37da24b8d7..dae5521b77 100644 --- a/unittests/checktime_tests.cpp +++ b/unittests/checktime_tests.cpp @@ -122,6 +122,55 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( checktime_fail_tests, T, validating_testers ) { t BOOST_REQUIRE_EQUAL( t.validate(), true ); } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE( checktime_interrupt_test) { try { + savanna_tester t; + savanna_tester other; + auto block = t.produce_block(); + other.push_block(block); + t.create_account( "testapi"_n ); + t.set_code( "testapi"_n, test_contracts::test_api_wasm() ); + block = t.produce_block(); + other.push_block(block); + + auto [trace, b] = CALL_TEST_FUNCTION_WITH_BLOCK( t, "test_checktime", "checktime_pass", {}); + BOOST_REQUIRE_EQUAL( b->transactions.size(), 1 ); + + // Make a copy of the valid block and swicth the checktime_pass transaction with checktime_failure + auto copy_b = std::make_shared(b->clone()); + auto signed_tx = std::get(copy_b->transactions.back().trx).get_signed_transaction(); + auto& act = signed_tx.actions.back(); + constexpr chain::name checktime_fail_n{WASM_TEST_ACTION("test_checktime", "checktime_failure")}; + act.name = checktime_fail_n; + act.data = fc::raw::pack(10000000000000000000ULL); + // Re-sign the transaction + signed_tx.signatures.clear(); + signed_tx.sign(t.get_private_key("testapi"_n, "active"), t.get_chain_id()); + // Replace the transaction + auto new_packed_tx = packed_transaction(signed_tx); + copy_b->transactions.back().trx = std::move(new_packed_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 = calculate_merkle( std::move(trx_digests) ); + // Re-sign the block + copy_b->producer_signature = t.get_private_key(config::system_account_name, "active").sign(copy_b->calculate_id()); + + std::thread th( [&c=*other.control]() { + std::this_thread::sleep_for( std::chrono::milliseconds(50) ); + c.interrupt_transaction(); + } ); + + // apply block, caught in an "infinite" loop + BOOST_CHECK_EXCEPTION( other.push_block(copy_b), fc::exception, + [](const fc::exception& e) { return e.code() == interrupt_exception::code_value; } ); + + th.join(); + +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_CASE_TEMPLATE( checktime_pause_max_trx_cpu_extended_test, T, testers ) { try { fc::temp_directory tempdir; auto conf_genesis = tester::default_config( tempdir ); diff --git a/unittests/test-contracts/payloadless/payloadless.abi b/unittests/test-contracts/payloadless/payloadless.abi index aafa35c171..f78c5c42f0 100644 --- a/unittests/test-contracts/payloadless/payloadless.abi +++ b/unittests/test-contracts/payloadless/payloadless.abi @@ -8,6 +8,11 @@ "base": "", "fields": [] }, + { + "name": "doitforever", + "base": "", + "fields": [] + }, { "name": "doitslow", "base": "", @@ -20,6 +25,11 @@ "type": "doit", "ricardian_contract": "" }, + { + "name": "doitforever", + "type": "doitforever", + "ricardian_contract": "" + }, { "name": "doitslow", "type": "doitslow", diff --git a/unittests/test-contracts/payloadless/payloadless.cpp b/unittests/test-contracts/payloadless/payloadless.cpp index e9556ca5ed..d7dcc99d4d 100644 --- a/unittests/test-contracts/payloadless/payloadless.cpp +++ b/unittests/test-contracts/payloadless/payloadless.cpp @@ -50,3 +50,17 @@ void payloadless::doitslow() { } } +void payloadless::doitforever() { + print("Im a payloadless forever action"); + constexpr size_t max_cpu_prime = std::numeric_limits::max(); + + while (true) { + for (size_t p = 2; p <= max_cpu_prime; p += 1) { + if (is_prime(p) && is_mersenne_prime(p)) { + // We need to keep an eye on this to make sure it doesn't get optimized out. So far so good. + //eosio::print_f(" %u", p); + } + } + } +} + diff --git a/unittests/test-contracts/payloadless/payloadless.hpp b/unittests/test-contracts/payloadless/payloadless.hpp index 0fea87a29b..10f696fded 100644 --- a/unittests/test-contracts/payloadless/payloadless.hpp +++ b/unittests/test-contracts/payloadless/payloadless.hpp @@ -11,4 +11,7 @@ class [[eosio::contract]] payloadless : public eosio::contract { [[eosio::action]] void doitslow(); + + [[eosio::action]] + void doitforever(); }; diff --git a/unittests/test-contracts/payloadless/payloadless.wasm b/unittests/test-contracts/payloadless/payloadless.wasm old mode 100644 new mode 100755 index 53cc07f560..6c8396afb6 Binary files a/unittests/test-contracts/payloadless/payloadless.wasm and b/unittests/test-contracts/payloadless/payloadless.wasm differ