diff --git a/libraries/chain/include/eosio/chain/transaction_context.hpp b/libraries/chain/include/eosio/chain/transaction_context.hpp index d9cdff7b5e4..4f3c10442f6 100644 --- a/libraries/chain/include/eosio/chain/transaction_context.hpp +++ b/libraries/chain/include/eosio/chain/transaction_context.hpp @@ -97,7 +97,8 @@ namespace eosio { namespace chain { 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 )const; - void validate_account_cpu_usage( int64_t billed_us, int64_t account_cpu_limit, bool estimate )const; + void validate_account_cpu_usage( int64_t billed_us, int64_t account_cpu_limit )const; + void validate_account_cpu_usage_estimate( int64_t billed_us, int64_t account_cpu_limit )const; void disallow_transaction_extensions( const char* error_msg )const; diff --git a/libraries/chain/transaction_context.cpp b/libraries/chain/transaction_context.cpp index 17d09d4a4dc..63a6a78ea6e 100644 --- a/libraries/chain/transaction_context.cpp +++ b/libraries/chain/transaction_context.cpp @@ -174,11 +174,11 @@ namespace eosio { namespace chain { if( !explicit_billed_cpu_time ) { // Fail early if amount of the previous speculative execution is within 10% of remaining account cpu available - int64_t validate_account_cpu_limit = account_cpu_limit - subjective_cpu_bill_us; + int64_t validate_account_cpu_limit = account_cpu_limit - subjective_cpu_bill_us + leeway.count(); // Add leeway to allow powerup if( validate_account_cpu_limit > 0 ) validate_account_cpu_limit -= EOS_PERCENT( validate_account_cpu_limit, 10 * config::percent_1 ); if( validate_account_cpu_limit < 0 ) validate_account_cpu_limit = 0; - validate_account_cpu_usage( billed_cpu_time_us, validate_account_cpu_limit, true ); + validate_account_cpu_usage_estimate( billed_cpu_time_us, validate_account_cpu_limit ); } eager_net_limit = (eager_net_limit/8)*8; // Round down to nearest multiple of word size (8 bytes) so check_net_usage can be efficient @@ -439,34 +439,65 @@ namespace eosio { namespace chain { ); } - validate_account_cpu_usage( billed_us, account_cpu_limit, false ); + validate_account_cpu_usage( billed_us, account_cpu_limit ); } } - void transaction_context::validate_account_cpu_usage( int64_t billed_us, int64_t account_cpu_limit, bool estimate )const { + void transaction_context::validate_account_cpu_usage( int64_t billed_us, int64_t account_cpu_limit )const { if( (billed_us > 0) && !control.skip_trx_checks() ) { const bool cpu_limited_by_account = (account_cpu_limit <= objective_duration_limit.count()); if( !cpu_limited_by_account && (billing_timer_exception_code == block_cpu_usage_exceeded::code_value) ) { EOS_ASSERT( billed_us <= objective_duration_limit.count(), block_cpu_usage_exceeded, - "${desc} CPU time (${billed} us) is greater than the billable CPU time left in the block (${billable} us)", - ("desc", (estimate ? "estimated" : "billed"))("billed", billed_us)( "billable", objective_duration_limit.count() ) + "billed CPU time (${billed} us) is greater than the billable CPU time left in the block (${billable} us)", + ("billed", billed_us)( "billable", objective_duration_limit.count() ) ); } else { if( cpu_limit_due_to_greylist && cpu_limited_by_account ) { EOS_ASSERT( billed_us <= account_cpu_limit, greylist_cpu_usage_exceeded, - "${desc} CPU time (${billed} us) is greater than the maximum greylisted billable CPU time for the transaction (${billable} us)", - ("desc", (estimate ? "estimated" : "billed"))("billed", billed_us)( "billable", account_cpu_limit ) + "billed CPU time (${billed} us) is greater than the maximum greylisted billable CPU time for the transaction (${billable} us)", + ("billed", billed_us)( "billable", account_cpu_limit ) ); } else { // exceeds trx.max_cpu_usage_ms or cfg.max_transaction_cpu_usage if objective_duration_limit is greater const int64_t cpu_limit = (cpu_limited_by_account ? account_cpu_limit : objective_duration_limit.count()); EOS_ASSERT( billed_us <= cpu_limit, tx_cpu_usage_exceeded, - "${desc} CPU time (${billed} us) is greater than the maximum billable CPU time for the transaction (${billable} us)", - ("desc", (estimate ? "estimated" : "billed"))("billed", billed_us)( "billable", cpu_limit ) + "billed CPU time (${billed} us) is greater than the maximum billable CPU time for the transaction (${billable} us)", + ("billed", billed_us)( "billable", cpu_limit ) + ); + } + } + } + } + + void transaction_context::validate_account_cpu_usage_estimate( int64_t prev_billed_us, int64_t account_cpu_limit )const { + // prev_billed_us can be 0, but so can account_cpu_limit + if( (prev_billed_us >= 0) && !control.skip_trx_checks() ) { + const bool cpu_limited_by_account = (account_cpu_limit <= objective_duration_limit.count()); + + if( !cpu_limited_by_account && (billing_timer_exception_code == block_cpu_usage_exceeded::code_value) ) { + EOS_ASSERT( prev_billed_us < objective_duration_limit.count(), + block_cpu_usage_exceeded, + "estimated CPU time (${billed} us) is not less than the billable CPU time left in the block (${billable} us)", + ("billed", prev_billed_us)( "billable", objective_duration_limit.count() ) + ); + } else { + if( cpu_limit_due_to_greylist && cpu_limited_by_account ) { + EOS_ASSERT( prev_billed_us < account_cpu_limit, + greylist_cpu_usage_exceeded, + "estimated CPU time (${billed} us) is not less than the maximum greylisted billable CPU time for the transaction (${billable} us)", + ("billed", prev_billed_us)( "billable", account_cpu_limit ) + ); + } else { + // exceeds trx.max_cpu_usage_ms or cfg.max_transaction_cpu_usage if objective_duration_limit is greater + const int64_t cpu_limit = (cpu_limited_by_account ? account_cpu_limit : objective_duration_limit.count()); + EOS_ASSERT( prev_billed_us < cpu_limit, + tx_cpu_usage_exceeded, + "estimated CPU time (${billed} us) is not less than the maximum billable CPU time for the transaction (${billable} us)", + ("billed", prev_billed_us)( "billable", cpu_limit ) ); } } diff --git a/plugins/producer_plugin/include/eosio/producer_plugin/subjective_billing.hpp b/plugins/producer_plugin/include/eosio/producer_plugin/subjective_billing.hpp index 0be8de26983..911a99a177e 100644 --- a/plugins/producer_plugin/include/eosio/producer_plugin/subjective_billing.hpp +++ b/plugins/producer_plugin/include/eosio/producer_plugin/subjective_billing.hpp @@ -60,6 +60,7 @@ class subjective_billing { trx_cache_index _trx_cache_index; account_subjective_bill_cache _account_subjective_bill_cache; block_subjective_bill_cache _block_subjective_bill_cache; + std::set _disabled_accounts; private: uint32_t time_ordinal_for( const fc::time_point& t ) const { @@ -112,12 +113,13 @@ class subjective_billing { public: void disable() { _disabled = true; } + void disable_account( chain::account_name a ) { _disabled_accounts.emplace( a ); } /// @param in_pending_block pass true if pt's bill time is accounted for in the pending block void subjective_bill( const transaction_id_type& id, const fc::time_point& expire, const account_name& first_auth, const fc::microseconds& elapsed, bool in_pending_block ) { - if( !_disabled ) { + if( !_disabled && !_disabled_accounts.count( first_auth ) ) { uint32_t bill = std::max( 0, elapsed.count() ); auto p = _trx_cache_index.emplace( trx_cache_entry{id, @@ -135,7 +137,7 @@ class subjective_billing { void subjective_bill_failure( const account_name& first_auth, const fc::microseconds& elapsed, const fc::time_point& now ) { - if( !_disabled ) { + if( !_disabled && !_disabled_accounts.count( first_auth ) ) { uint32_t bill = std::max( 0, elapsed.count() ); const auto time_ordinal = time_ordinal_for(now); _account_subjective_bill_cache[first_auth].expired_accumulator.add(bill, time_ordinal, expired_accumulator_average_window); @@ -143,7 +145,7 @@ class subjective_billing { } uint32_t get_subjective_bill( const account_name& first_auth, const fc::time_point& now ) const { - if( _disabled ) return 0; + if( _disabled || _disabled_accounts.count( first_auth ) ) return 0; const auto time_ordinal = time_ordinal_for(now); const subjective_billing_info* sub_bill_info = nullptr; auto aitr = _account_subjective_bill_cache.find( first_auth ); diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index 68395bc9aab..6da601f263e 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -218,6 +218,8 @@ class producer_plugin_impl : public std::enable_shared_from_thisget_executor(), chain.get_chain_id(), fc::microseconds( max_trx_cpu_usage ), chain.configured_subjective_signature_length_limit() ); - auto trx_id = trx->id(); boost::asio::post(_thread_pool->get_executor(), [self = this, future{std::move(future)}, persist_until_expired, - next{std::move(next)}, trx_id]() mutable { + next{std::move(next)}, trx]() mutable { if( future.valid() ) { future.wait(); - app().post( priority::low, [self, future{std::move(future)}, persist_until_expired, next{std::move( next )}, trx_id]() mutable { - auto exception_handler = [&next, trx_id](fc::exception_ptr ex) { - fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid} : ${why} ", - ("txid", trx_id)("why",ex->what())); - fc_dlog(_trx_failed_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid} : ${why} ", - ("txid", trx_id)("why",ex->what())); + app().post( priority::low, [self, future{std::move(future)}, persist_until_expired, next{std::move( next )}, trx{std::move(trx)}]() mutable { + auto exception_handler = [&next, trx{std::move(trx)}](fc::exception_ptr ex) { + fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid}, auth: ${a} : ${why} ", + ("txid", trx->id())("a",trx->get_transaction().first_authorizer())("why",ex->what())); + fc_dlog(_trx_failed_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid}, auth: ${a} : ${why} ", + ("txid", trx->id())("a",trx->get_transaction().first_authorizer())("why",ex->what())); next(ex); }; try { @@ -473,34 +474,40 @@ class producer_plugin_impl : public std::enable_shared_from_this()) { _transaction_ack_channel.publish(priority::low, std::pair(response.get(), trx)); if (_pending_block_mode == pending_block_mode::producing) { - fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is REJECTING tx: ${txid} : ${why} ", + fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is REJECTING tx: ${txid}, auth: ${a} : ${why} ", ("block_num", chain.head_block_num() + 1) ("prod", get_pending_block_producer()) ("txid", trx->id()) + ("a", trx->packed_trx()->get_transaction().first_authorizer()) ("why",response.get()->what())); - fc_dlog(_trx_failed_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is REJECTING tx: ${txid} : ${why} ", + fc_dlog(_trx_failed_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is REJECTING tx: ${txid}, auth: ${a} : ${why} ", ("block_num", chain.head_block_num() + 1) ("prod", get_pending_block_producer()) ("txid", trx->id()) + ("a", trx->packed_trx()->get_transaction().first_authorizer()) ("why",response.get()->what())); } else { - fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid} : ${why} ", + fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid}, auth: ${a} : ${why} ", ("txid", trx->id()) + ("a", trx->packed_trx()->get_transaction().first_authorizer()) ("why",response.get()->what())); - fc_dlog(_trx_failed_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid} : ${why} ", + fc_dlog(_trx_failed_trace_log, "[TRX_TRACE] Speculative execution is REJECTING tx: ${txid}, auth: ${a} : ${why} ", ("txid", trx->id()) + ("a", trx->packed_trx()->get_transaction().first_authorizer()) ("why",response.get()->what())); } } else { _transaction_ack_channel.publish(priority::low, std::pair(nullptr, trx)); if (_pending_block_mode == pending_block_mode::producing) { - fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is ACCEPTING tx: ${txid}", + fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is ACCEPTING tx: ${txid}, auth: ${a}", ("block_num", chain.head_block_num() + 1) ("prod", get_pending_block_producer()) - ("txid", trx->id())); + ("txid", trx->id()) + ("a", trx->packed_trx()->get_transaction().first_authorizer())); } else { - fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Speculative execution is ACCEPTING tx: ${txid}", - ("txid", trx->id())); + fc_dlog(_trx_successful_trace_log, "[TRX_TRACE] Speculative execution is ACCEPTING tx: ${txid}, auth: ${a}", + ("txid", trx->id()) + ("a", trx->packed_trx()->get_transaction().first_authorizer())); } } }; @@ -538,12 +545,17 @@ class producer_plugin_impl : public std::enable_shared_from_thispacked_trx()->get_transaction().first_authorizer(); uint32_t sub_bill = 0; - if( _pending_block_mode != pending_block_mode::producing) + if( !disable_subjective_billing ) sub_bill = _subjective_billing.get_subjective_bill( first_auth, fc::time_point::now() ); auto trace = chain.push_transaction( trx, deadline, trx->billed_cpu_time_us, false, sub_bill ); + fc_dlog( _trx_failed_trace_log, "Subjective bill for ${a}: ${b} elapsed ${t}us", ("a",first_auth)("b",sub_bill)("t",trace->elapsed)); if( trace->except ) { if( exception_is_exhausted( *trace->except, deadline_is_subjective )) { _pending_incoming_transactions.add( trx, persist_until_expired, next ); @@ -713,8 +725,14 @@ void producer_plugin::set_program_options( "Maximum size (in MiB) of the incoming transaction queue. Exceeding this value will subjectively drop transaction with resource exhaustion.") ("disable-api-persisted-trx", bpo::bool_switch()->default_value(false), "Disable the re-apply of API transactions.") - ("disable-subjective-billing", bpo::bool_switch()->default_value(false), - "Disable subjective billing.") + ("disable-subjective-billing", bpo::value()->default_value(true), + "Disable subjective CPU billing for API/P2P transactions") + ("disable-subjective-account-billing", boost::program_options::value>()->composing()->multitoken(), + "Account which is excluded from subjective CPU billing") + ("disable-subjective-p2p-billing", bpo::value()->default_value(true), + "Disable subjective CPU billing for P2P transactions") + ("disable-subjective-api-billing", bpo::value()->default_value(true), + "Disable subjective CPU billing for API transactions") ("producer-threads", bpo::value()->default_value(config::default_controller_thread_pool_size), "Number of worker threads in producer thread pool") ("snapshots-dir", bpo::value()->default_value("snapshots"), @@ -904,7 +922,25 @@ void producer_plugin::plugin_initialize(const boost::program_options::variables_ my->_incoming_defer_ratio = options.at("incoming-defer-ratio").as(); my->_disable_persist_until_expired = options.at("disable-api-persisted-trx").as(); - if( options.at("disable-subjective-billing").as() ) my->_subjective_billing.disable(); + bool disable_subjective_billing = options.at("disable-subjective-billing").as(); + my->_disable_subjective_p2p_billing = options.at("disable-subjective-p2p-billing").as(); + my->_disable_subjective_api_billing = options.at("disable-subjective-api-billing").as(); + dlog( "disable-subjective-billing: ${s}, disable-subjective-p2p-billing: ${p2p}, disable-subjective-api-billing: ${api}", + ("s", disable_subjective_billing)("p2p", my->_disable_subjective_p2p_billing)("api", my->_disable_subjective_api_billing) ); + if( !disable_subjective_billing ) { + my->_disable_subjective_p2p_billing = my->_disable_subjective_api_billing = false; + } else if( !my->_disable_subjective_p2p_billing || !my->_disable_subjective_api_billing ) { + disable_subjective_billing = false; + } + if( disable_subjective_billing ) { + my->_subjective_billing.disable(); + ilog( "Subjective CPU billing disabled" ); + } else if( !my->_disable_subjective_p2p_billing && !my->_disable_subjective_api_billing ) { + ilog( "Subjective CPU billing enabled" ); + } else { + if( my->_disable_subjective_p2p_billing ) ilog( "Subjective CPU billing of P2P trxs disabled " ); + if( my->_disable_subjective_api_billing ) ilog( "Subjective CPU billing of API trxs disabled " ); + } auto thread_pool_size = options.at( "producer-threads" ).as(); EOS_ASSERT( thread_pool_size > 0, plugin_config_exception, @@ -964,6 +1000,13 @@ void producer_plugin::plugin_initialize(const boost::program_options::variables_ chain.set_greylist_limit( greylist_limit ); } + if( options.count("disable-subjective-account-billing") ) { + std::vector accounts = options["disable-subjective-account-billing"].as>(); + for( const auto& a : accounts ) { + my->_subjective_billing.disable_account( account_name(a) ); + } + } + } FC_LOG_AND_RETHROW() } void producer_plugin::plugin_startup() @@ -1737,17 +1780,15 @@ bool producer_plugin_impl::remove_expired_blacklisted_trxs( const fc::time_point } namespace { -// track multiple deadline / transaction cpu exceeded exceptions on unapplied transactions +// track multiple failures on unapplied transactions class account_failures { public: constexpr static uint32_t max_failures_per_account = 3; void add( const account_name& n, int64_t exception_code ) { - if( exception_code == deadline_exception::code_value || exception_code == tx_cpu_usage_exceeded::code_value ) { - auto& fa = failed_accounts[n]; - ++fa.num_failures; - fa.add( exception_code ); - } + auto& fa = failed_accounts[n]; + ++fa.num_failures; + fa.add( n, exception_code ); } // return true if exceeds max_failures_per_account and should be dropped @@ -1771,6 +1812,14 @@ class account_failures { if( !reason.empty() ) reason += ", "; reason += "tx_cpu_usage"; } + if( e.second.is_eosio_assert() ) { + if( !reason.empty() ) reason += ", "; + reason += "assert"; + } + if( e.second.is_other() ) { + if( !reason.empty() ) reason += ", "; + reason += "other"; + } fc_dlog( _log, "Dropped ${n} trxs, account: ${a}, reason: ${r} exceeded", ("n", e.second.num_failures - max_failures_per_account)("a", e.first)("r", reason) ); } @@ -1782,18 +1831,30 @@ class account_failures { struct account_failure { enum class ex_fields : uint8_t { ex_deadline_exception = 1, - ex_tx_cpu_usage_exceeded = 2 + ex_tx_cpu_usage_exceeded = 2, + ex_eosio_assert_exception = 4, + ex_other_exception = 8 }; - void add(int64_t exception_code = 0) { + void add( const account_name& n, int64_t exception_code ) { if( exception_code == tx_cpu_usage_exceeded::code_value ) { ex_flags = set_field( ex_flags, ex_fields::ex_tx_cpu_usage_exceeded ); } else if( exception_code == deadline_exception::code_value ) { ex_flags = set_field( ex_flags, ex_fields::ex_deadline_exception ); + } else if( exception_code == eosio_assert_message_exception::code_value || + exception_code == eosio_assert_code_exception::code_value ) { + ex_flags = set_field( ex_flags, ex_fields::ex_eosio_assert_exception ); + } else { + ex_flags = set_field( ex_flags, ex_fields::ex_other_exception ); + fc_dlog( _log, "Failed trx, account: ${a}, reason: ${r}", + ("a", n)("r", exception_code) ); } } + bool is_deadline() const { return has_field( ex_flags, ex_fields::ex_deadline_exception ); } bool is_tx_cpu_usage() const { return has_field( ex_flags, ex_fields::ex_tx_cpu_usage_exceeded ); } + bool is_eosio_assert() const { return has_field( ex_flags, ex_fields::ex_eosio_assert_exception ); } + bool is_other() const { return has_field( ex_flags, ex_fields::ex_other_exception ); } uint32_t num_failures = 0; uint8_t ex_flags = 0; @@ -1852,6 +1913,7 @@ bool producer_plugin_impl::process_unapplied_trxs( const fc::time_point& deadlin const uint32_t sub_bill = 0; auto trace = chain.push_transaction( trx, trx_deadline, prev_billed_cpu_time_us, false, sub_bill ); + fc_dlog( _trx_failed_trace_log, "Subjective unapplied bill for ${a}: ${b} prev ${t}us", ("a",first_auth)("b",prev_billed_cpu_time_us)("t",trace->elapsed)); if( trace->except ) { if( exception_is_exhausted( *trace->except, deadline_is_subjective ) ) { if( block_is_exhausted() ) { @@ -1860,6 +1922,7 @@ bool producer_plugin_impl::process_unapplied_trxs( const fc::time_point& deadlin break; } } else { + fc_dlog( _trx_failed_trace_log, "Subjective unapplied bill for failed ${a}: ${b} prev ${t}us", ("a",first_auth)("b",prev_billed_cpu_time_us)("t",trace->elapsed)); auto failure_code = trace->except->code(); if( failure_code != tx_duplicate::code_value ) { // this failed our configured maximum transaction time, we don't want to replay it @@ -1874,6 +1937,7 @@ bool producer_plugin_impl::process_unapplied_trxs( const fc::time_point& deadlin continue; } } else { + fc_dlog( _trx_successful_trace_log, "Subjective unapplied bill for success ${a}: ${b} prev ${t}us", ("a",first_auth)("b",prev_billed_cpu_time_us)("t",trace->elapsed)); // if db_read_mode SPECULATIVE then trx is in the pending block and not immediately reverted _subjective_billing.subjective_bill( trx->id(), trx->packed_trx()->expiration(), first_auth, trace->elapsed, chain.get_read_mode() == chain::db_read_mode::SPECULATIVE ); diff --git a/unittests/wasm_tests.cpp b/unittests/wasm_tests.cpp index 655971bd64d..d816f85b54d 100644 --- a/unittests/wasm_tests.cpp +++ b/unittests/wasm_tests.cpp @@ -2009,20 +2009,11 @@ BOOST_AUTO_TEST_CASE( billed_cpu_test ) try { cpu_limit = mgr.get_account_cpu_limit_ex(acc).first.max; cpu_limit -= EOS_PERCENT( cpu_limit, 10 * config::percent_1 ); // transaction_context verifies within 10%, so subtract 10% out - ptrx = create_trx(0); - BOOST_CHECK_LT( cpu_limit+1, max_cpu_time_us ); // needs to be less or this just tests the same thing as max_cpu_time_us test above - // indicate non-explicit billing with 1 more than our account cpu limit, triggers optimization check #8638 and fails trx - BOOST_CHECK_EXCEPTION( push_trx( ptrx, fc::time_point::maximum(), cpu_limit+1, false, 0 ), tx_cpu_usage_exceeded, - fc_exception_message_starts_with("estimated") ); - // indicate non-explicit billing with 1 more (subjective) than our account cpu limit, triggers optimization check #8638 and fails trx - BOOST_CHECK_EXCEPTION( push_trx( ptrx, fc::time_point::maximum(), cpu_limit, false, 1 ), tx_cpu_usage_exceeded, - fc_exception_message_starts_with("estimated") ); - ptrx = create_trx(0); BOOST_CHECK_LT( cpu_limit, max_cpu_time_us ); - // indicate non-explicit billing at our account cpu limit, will allow this trx to run, but only bills for actual use - auto r = push_trx( ptrx, fc::time_point::maximum(), cpu_limit, false, 0 ); - BOOST_CHECK_LT( r->receipt->cpu_usage_us, cpu_limit ); // verify not billed at provided bill amount when explicit_billed_cpu_time=false + // indicate non-explicit billing at one less than our account cpu limit, will allow this trx to run, but only bills for actual use + auto r = push_trx( ptrx, fc::time_point::maximum(), cpu_limit-1, false, 0 ); + BOOST_CHECK_LT( r->receipt->cpu_usage_us, cpu_limit-1 ); // verify not billed at provided bill amount when explicit_billed_cpu_time=false chain.produce_block(); chain.produce_block( fc::days(1) ); // produce for one day to reset account cpu @@ -2034,6 +2025,64 @@ BOOST_AUTO_TEST_CASE( billed_cpu_test ) try { BOOST_CHECK_EXCEPTION( push_trx( ptrx, fc::time_point::maximum(), cpu_limit+1, true, 0 ), tx_cpu_usage_exceeded, fc_exception_message_starts_with("billed") ); + // leeway and subjective billing interaction tests + auto leeway = fc::microseconds(config::default_subjective_cpu_leeway_us); + chain.control->set_subjective_cpu_leeway(leeway); + + // Allow transaction with billed cpu less than 90% of (account cpu limit + leeway - subjective bill) + chain.produce_block(); + chain.produce_block( fc::days(1) ); // produce for one day to reset account cpu + ptrx = create_trx(0); + uint32_t combined_cpu_limit = mgr.get_account_cpu_limit_ex(acc).first.max + leeway.count(); + uint32_t subjective_cpu_bill_us = leeway.count(); + uint32_t billed_cpu_time_us = EOS_PERCENT( (combined_cpu_limit - subjective_cpu_bill_us), 89 *config::percent_1 ); + push_trx( ptrx, fc::time_point::maximum(), billed_cpu_time_us, false, subjective_cpu_bill_us ); + + // Allow transaction with billed cpu less than 90% of (account cpu limit + leeway) if subject bill is 0 + chain.control->set_subjective_cpu_leeway(leeway); + chain.produce_block(); + chain.produce_block( fc::days(1) ); // produce for one day to reset account cpu + ptrx = create_trx(0); + combined_cpu_limit = mgr.get_account_cpu_limit_ex(acc).first.max + leeway.count(); + subjective_cpu_bill_us = 0; + billed_cpu_time_us = EOS_PERCENT( combined_cpu_limit - subjective_cpu_bill_us, 89 *config::percent_1 ); + push_trx( ptrx, fc::time_point::maximum(), billed_cpu_time_us, false, subjective_cpu_bill_us ); + + // Disallow transaction with billed cpu equal to 90% of (account cpu limit + leeway - subjective bill) + chain.produce_block(); + chain.produce_block( fc::days(1) ); // produce for one day to reset account cpu + ptrx = create_trx(0); + cpu_limit = mgr.get_account_cpu_limit_ex(acc).first.max; + combined_cpu_limit = cpu_limit + leeway.count(); + subjective_cpu_bill_us = cpu_limit; + billed_cpu_time_us = EOS_PERCENT( combined_cpu_limit - subjective_cpu_bill_us, 90 * config::percent_1 ); + BOOST_CHECK_EXCEPTION(push_trx( ptrx, fc::time_point::maximum(), billed_cpu_time_us, false, subjective_cpu_bill_us ), tx_cpu_usage_exceeded, + fc_exception_message_starts_with("estimated") ); + + // Disallow transaction with billed cpu greater 90% of (account cpu limit + leeway - subjective bill) + subjective_cpu_bill_us = 0; + billed_cpu_time_us = EOS_PERCENT( combined_cpu_limit - subjective_cpu_bill_us, 91 * config::percent_1 ); + BOOST_CHECK_EXCEPTION(push_trx( ptrx, fc::time_point::maximum(), billed_cpu_time_us, false, subjective_cpu_bill_us ), tx_cpu_usage_exceeded, + fc_exception_message_starts_with("estimated") ); + + // Test when cpu limit is 0 + chain.push_action( config::system_account_name, N(setalimits), config::system_account_name, fc::mutable_variant_object() + ("account", acc) + ("ram_bytes", -1) + ("net_weight", 75) + ("cpu_weight", 0) + ); + + chain.produce_block(); + chain.produce_block( fc::days(1) ); // produce for one day to reset account cpu + + // Allow transaction with billed cpu less than 90% of leeway subjective bill being 0 to run but fail it if no cpu is staked afterwards + ptrx = create_trx(0); + subjective_cpu_bill_us = 0; + billed_cpu_time_us = EOS_PERCENT( leeway.count(), 89 *config::percent_1 ); + BOOST_CHECK_EXCEPTION(push_trx( ptrx, fc::time_point::maximum(), billed_cpu_time_us, false, subjective_cpu_bill_us ), tx_cpu_usage_exceeded, + fc_exception_message_starts_with("billed") ); + } FC_LOG_AND_RETHROW()