diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 461953c28d5..a6cf5e4b198 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1418,7 +1418,8 @@ struct controller_impl { transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx, fc::time_point deadline, uint32_t billed_cpu_time_us, - bool explicit_billed_cpu_time ) + bool explicit_billed_cpu_time, + fc::optional explicit_net_usage_words ) { EOS_ASSERT(deadline != fc::time_point(), transaction_exception, "deadline cannot be uninitialized"); @@ -1450,13 +1451,18 @@ struct controller_impl { trace = trx_context.trace; try { if( trx->implicit ) { + EOS_ASSERT( !explicit_net_usage_words.valid(), transaction_exception, "NET usage cannot be explicitly set for implicit transactions" ); trx_context.init_for_implicit_trx(); trx_context.enforce_whiteblacklist = false; } else { bool skip_recording = replay_head_time && (time_point(trn.expiration) <= *replay_head_time); - trx_context.init_for_input_trx( trx->packed_trx()->get_unprunable_size(), - trx->packed_trx()->get_prunable_size(), - skip_recording); + if( explicit_net_usage_words ) { + trx_context.init_for_input_trx_with_explicit_net( *explicit_net_usage_words, skip_recording ); + } else { + trx_context.init_for_input_trx( trx->packed_trx()->get_unprunable_size(), + trx->packed_trx()->get_prunable_size(), + skip_recording ); + } } trx_context.delay = fc::seconds(trn.delay_sec); @@ -1658,7 +1664,7 @@ struct controller_impl { in_trx_requiring_checks = old_value; }); in_trx_requiring_checks = true; - push_transaction( onbtrx, fc::time_point::maximum(), gpo.configuration.min_transaction_cpu_usage, true ); + push_transaction( onbtrx, fc::time_point::maximum(), gpo.configuration.min_transaction_cpu_usage, true, {} ); } catch( const std::bad_alloc& e ) { elog( "on block transaction failed due to a std::bad_alloc" ); throw; @@ -1890,6 +1896,8 @@ struct controller_impl { transaction_trace_ptr trace; + bool explicit_net = self.skip_trx_checks(); + size_t packed_idx = 0; const auto& trx_receipts = pending->_block_stage.get()._pending_trx_receipts; for( const auto& receipt : b->transactions ) { @@ -1899,7 +1907,11 @@ struct controller_impl { : ( !!std::get<0>( trx_metas.at( packed_idx ) ) ? std::get<0>( trx_metas.at( packed_idx ) ) : std::get<1>( trx_metas.at( packed_idx ) ).get() ) ); - trace = push_transaction( trx_meta, fc::time_point::maximum(), receipt.cpu_usage_us, true ); + fc::optional explicit_net_usage_words; + if( explicit_net ) { + explicit_net_usage_words = receipt.net_usage_words.value; + } + trace = push_transaction( trx_meta, fc::time_point::maximum(), receipt.cpu_usage_us, true, explicit_net_usage_words ); ++packed_idx; } else if( receipt.trx.contains() ) { trace = push_scheduled_transaction( receipt.trx.get(), fc::time_point::maximum(), receipt.cpu_usage_us, true ); @@ -2679,7 +2691,7 @@ transaction_trace_ptr controller::push_transaction( const transaction_metadata_p validate_db_available_size(); EOS_ASSERT( get_read_mode() != db_read_mode::IRREVERSIBLE, transaction_type_exception, "push transaction not allowed in irreversible mode" ); EOS_ASSERT( trx && !trx->implicit && !trx->scheduled, transaction_type_exception, "Implicit/Scheduled transaction not allowed" ); - return my->push_transaction(trx, deadline, billed_cpu_time_us, explicit_billed_cpu_time ); + return my->push_transaction(trx, deadline, billed_cpu_time_us, explicit_billed_cpu_time, {} ); } transaction_trace_ptr controller::push_scheduled_transaction( const transaction_id_type& trxid, fc::time_point deadline, diff --git a/libraries/chain/include/eosio/chain/transaction_context.hpp b/libraries/chain/include/eosio/chain/transaction_context.hpp index 04332cb3472..5517eb83f52 100644 --- a/libraries/chain/include/eosio/chain/transaction_context.hpp +++ b/libraries/chain/include/eosio/chain/transaction_context.hpp @@ -31,7 +31,9 @@ namespace eosio { namespace chain { class transaction_context { private: - void init( uint64_t initial_net_usage); + void init( uint64_t initial_net_usage ); + + void init_for_input_trx_common( uint64_t initial_net_usage, bool skip_recording ); public: @@ -45,7 +47,10 @@ namespace eosio { namespace chain { void init_for_input_trx( uint64_t packed_trx_unprunable_size, uint64_t packed_trx_prunable_size, - bool skip_recording); + bool skip_recording ); + + void init_for_input_trx_with_explicit_net( uint32_t explicit_net_usage_words, + bool skip_recording ); void init_for_deferred_trx( fc::time_point published ); @@ -54,7 +59,17 @@ namespace eosio { namespace chain { void squash(); void undo(); - inline void add_net_usage( uint64_t u ) { net_usage += u; check_net_usage(); } + inline void add_net_usage( uint64_t u ) { + if( explicit_net_usage ) return; + net_usage += u; + check_net_usage(); + } + + inline void round_up_net_usage() { + if( explicit_net_usage ) return; + net_usage = ((net_usage + 7)/8)*8; // Round up to nearest multiple of word size (8 bytes) + check_net_usage(); + } void check_net_usage()const; @@ -156,6 +171,7 @@ namespace eosio { namespace chain { bool net_limit_due_to_greylist = false; uint64_t eager_net_limit = 0; uint64_t& net_usage; /// reference to trace->net_usage + bool explicit_net_usage = false; bool cpu_limit_due_to_greylist = false; diff --git a/libraries/chain/transaction_context.cpp b/libraries/chain/transaction_context.cpp index ed3f4e5b4c9..25346fd32e3 100644 --- a/libraries/chain/transaction_context.cpp +++ b/libraries/chain/transaction_context.cpp @@ -75,7 +75,7 @@ namespace eosio { namespace chain { } } - void transaction_context::init(uint64_t initial_net_usage) + void transaction_context::init( uint64_t initial_net_usage ) { EOS_ASSERT( !is_initialized, transaction_exception, "cannot initialize twice" ); @@ -197,7 +197,7 @@ namespace eosio { namespace chain { } published = control.pending_block_time(); - init( initial_net_usage); + init( initial_net_usage ); } void transaction_context::init_for_input_trx( uint64_t packed_trx_unprunable_size, @@ -230,6 +230,24 @@ namespace eosio { namespace chain { + static_cast(config::transaction_id_net_usage); } + init_for_input_trx_common( initial_net_usage, skip_recording ); + } + + void transaction_context::init_for_input_trx_with_explicit_net( uint32_t explicit_net_usage_words, + bool skip_recording ) + { + if( trx.transaction_extensions.size() > 0 ) { + disallow_transaction_extensions( "no transaction extensions supported yet for input transactions" ); + } + + explicit_net_usage = true; + net_usage = (static_cast(explicit_net_usage_words) * 8); + + init_for_input_trx_common( 0, skip_recording ); + } + + void transaction_context::init_for_input_trx_common( uint64_t initial_net_usage, bool skip_recording ) + { published = control.pending_block_time(); is_input = true; if (!control.skip_trx_checks()) { @@ -237,7 +255,7 @@ namespace eosio { namespace chain { control.validate_tapos(trx); validate_referenced_accounts( trx, enforce_whiteblacklist && control.is_producing_block() ); } - init( initial_net_usage); + init( initial_net_usage ); if (!skip_recording) record_transaction( id, trx.expiration ); /// checks for dupes } @@ -323,10 +341,10 @@ namespace eosio { namespace chain { billing_timer_exception_code = tx_cpu_usage_exceeded::code_value; } - net_usage = ((net_usage + 7)/8)*8; // Round up to nearest multiple of word size (8 bytes) - eager_net_limit = net_limit; - check_net_usage(); + + round_up_net_usage(); // Round up to nearest multiple of word size (8 bytes). + check_net_usage(); // Check that NET usage satisfies limits (even when explicit_net_usage is true). auto now = fc::time_point::now(); trace->elapsed = now - start; @@ -348,21 +366,19 @@ namespace eosio { namespace chain { } void transaction_context::check_net_usage()const { - if (!control.skip_trx_checks()) { - if( BOOST_UNLIKELY(net_usage > eager_net_limit) ) { - if ( net_limit_due_to_block ) { - EOS_THROW( block_net_usage_exceeded, - "not enough space left in block: ${net_usage} > ${net_limit}", - ("net_usage", net_usage)("net_limit", eager_net_limit) ); - } else if (net_limit_due_to_greylist) { - EOS_THROW( greylist_net_usage_exceeded, - "greylisted transaction net usage is too high: ${net_usage} > ${net_limit}", - ("net_usage", net_usage)("net_limit", eager_net_limit) ); - } else { - EOS_THROW( tx_net_usage_exceeded, - "transaction net usage is too high: ${net_usage} > ${net_limit}", - ("net_usage", net_usage)("net_limit", eager_net_limit) ); - } + if( BOOST_UNLIKELY(net_usage > eager_net_limit) ) { + if ( net_limit_due_to_block ) { + EOS_THROW( block_net_usage_exceeded, + "not enough space left in block: ${net_usage} > ${net_limit}", + ("net_usage", net_usage)("net_limit", eager_net_limit) ); + } else if (net_limit_due_to_greylist) { + EOS_THROW( greylist_net_usage_exceeded, + "greylisted transaction net usage is too high: ${net_usage} > ${net_limit}", + ("net_usage", net_usage)("net_limit", eager_net_limit) ); + } else { + EOS_THROW( tx_net_usage_exceeded, + "transaction net usage is too high: ${net_usage} > ${net_limit}", + ("net_usage", net_usage)("net_limit", eager_net_limit) ); } } } diff --git a/libraries/testing/include/eosio/testing/tester.hpp b/libraries/testing/include/eosio/testing/tester.hpp index cbd7432fbbd..f43a093b8ea 100644 --- a/libraries/testing/include/eosio/testing/tester.hpp +++ b/libraries/testing/include/eosio/testing/tester.hpp @@ -638,6 +638,18 @@ namespace eosio { namespace testing { bool skip_validate = false; }; + /** + * Utility predicate to check whether an fc::exception code is equivalent to a given value + */ + struct fc_exception_code_is { + fc_exception_code_is( int64_t code ) + : expected( code ) {} + + bool operator()( const fc::exception& ex ); + + int64_t expected; + }; + /** * Utility predicate to check whether an fc::exception message is equivalent to a given string */ diff --git a/libraries/testing/tester.cpp b/libraries/testing/tester.cpp index fc564e7713d..fe2d5fcbf63 100644 --- a/libraries/testing/tester.cpp +++ b/libraries/testing/tester.cpp @@ -1153,6 +1153,15 @@ namespace eosio { namespace testing { preactivate_protocol_features( preactivations ); } + bool fc_exception_code_is::operator()( const fc::exception& ex ) { + bool match = (ex.code() == expected); + if( !match ) { + auto message = ex.get_log().at( 0 ).get_message(); + BOOST_TEST_MESSAGE( "LOG: expected code: " << expected << ", actual code: " << ex.code() << ", message: " << message ); + } + return match; + } + bool fc_exception_message_is::operator()( const fc::exception& ex ) { auto message = ex.get_log().at( 0 ).get_message(); bool match = (message == expected); diff --git a/unittests/resource_limits_test.cpp b/unittests/resource_limits_test.cpp index ef7a71bcd5c..0914f785e26 100644 --- a/unittests/resource_limits_test.cpp +++ b/unittests/resource_limits_test.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include "fork_test_utilities.hpp" #include @@ -417,5 +419,141 @@ BOOST_AUTO_TEST_SUITE(resource_limits_test) } FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_CASE(light_net_validation) try { + tester main( setup_policy::preactivate_feature_and_new_bios ); + tester validator( setup_policy::none ); + tester validator2( setup_policy::none ); + + name test_account("alice"); + name test_account2("bob"); + + constexpr int64_t net_weight = 1; + constexpr int64_t other_net_weight = 1'000'000'000; + + signed_block_ptr trigger_block; + + // Create test account with limited NET quota + main.create_accounts( {test_account, test_account2} ); + main.push_action( config::system_account_name, N(setalimits), config::system_account_name, fc::mutable_variant_object() + ("account", test_account) + ("ram_bytes", -1) + ("net_weight", net_weight) + ("cpu_weight", -1) + ); + main.push_action( config::system_account_name, N(setalimits), config::system_account_name, fc::mutable_variant_object() + ("account", test_account2) + ("ram_bytes", -1) + ("net_weight", other_net_weight) + ("cpu_weight", -1) + ); + main.produce_block(); + + // Sync validator and validator2 nodes with the main node as of this state. + push_blocks( main, validator ); + push_blocks( main, validator2 ); + + // Restart validator2 node in light validation mode. + { + validator2.close(); + auto cfg = validator2.get_config(); + cfg.block_validation_mode = validation_mode::LIGHT; + validator2.init( cfg ); + } + + const auto& rlm = main.control->get_resource_limits_manager(); + + // NET quota of test account should be enough to support one but not two reqauth transactions. + int64_t before_net_usage = rlm.get_account_net_limit_ex( test_account ).first.used; + main.push_action( config::system_account_name, N(reqauth), test_account, fc::mutable_variant_object()("from", "alice"), 6 ); + int64_t after_net_usage = rlm.get_account_net_limit_ex( test_account ).first.used; + int64_t reqauth_net_usage_delta = after_net_usage - before_net_usage; + BOOST_REQUIRE_EXCEPTION( + main.push_action( config::system_account_name, N(reqauth), test_account, fc::mutable_variant_object()("from", "alice"), 7 ), + fc::exception, fc_exception_code_is( tx_net_usage_exceeded::code_value ) + ); + trigger_block = main.produce_block(); + + auto main_schedule_hash = main.control->head_block_state()->pending_schedule.schedule_hash; + auto main_block_mroot = main.control->head_block_state()->blockroot_merkle.get_root(); + + // Modify NET bill in transaction receipt within trigger block to cause the NET usage of test account to exceed its quota. + { + // Increase the NET usage in the reqauth transaction receipt. + trigger_block->transactions.back().net_usage_words.value = 2*((reqauth_net_usage_delta + 7)/8); // double the NET bill + + // Re-calculate the transaction merkle + deque trx_digests; + const auto& trxs = trigger_block->transactions; + for( const auto& a : trxs ) + trx_digests.emplace_back( a.digest() ); + trigger_block->transaction_mroot = merkle( move(trx_digests) ); + + // Re-sign the block + auto header_bmroot = digest_type::hash( std::make_pair( trigger_block->digest(), main_block_mroot ) ); + auto sig_digest = digest_type::hash( std::make_pair(header_bmroot, main_schedule_hash ) ); + trigger_block->producer_signature = main.get_private_key(config::system_account_name, "active").sign(sig_digest); + } + + // Push trigger block to validator node. + // This should fail because the NET bill calculated by the fully-validating node will differ from the one in the block. + { + auto bs = validator.control->create_block_state_future( trigger_block ); + validator.control->abort_block(); + BOOST_REQUIRE_EXCEPTION( + validator.control->push_block( bs, forked_branch_callback{}, trx_meta_cache_lookup{} ), + fc::exception, fc_exception_message_is( "receipt does not match" ) + ); + } + + // Push trigger block to validator2 node. + // This should still cause a NET failure, but will no longer be due to a receipt mismatch. + // Because validator2 is in light validation mode, it does not compute the NET bill itself and instead only relies on the value in the block. + // The failure will be due to failing check_net_usage within transaction_context::finalize because the NET bill in the block is too high. + { + auto bs = validator2.control->create_block_state_future( trigger_block ); + validator2.control->abort_block(); + BOOST_REQUIRE_EXCEPTION( + validator2.control->push_block( bs, forked_branch_callback{}, trx_meta_cache_lookup{} ), + fc::exception, fc_exception_code_is( tx_net_usage_exceeded::code_value ) + ); + } + + // Modify NET bill in transaction receipt within trigger block to be lower than what it originally was. + { + // Increase the NET usage in the reqauth transaction receipt. + trigger_block->transactions.back().net_usage_words.value = ((reqauth_net_usage_delta + 7)/8)/2; // half the original NET bill + + // Re-calculate the transaction merkle + deque trx_digests; + const auto& trxs = trigger_block->transactions; + for( const auto& a : trxs ) + trx_digests.emplace_back( a.digest() ); + trigger_block->transaction_mroot = merkle( move(trx_digests) ); + + // Re-sign the block + auto header_bmroot = digest_type::hash( std::make_pair( trigger_block->digest(), main_block_mroot ) ); + auto sig_digest = digest_type::hash( std::make_pair(header_bmroot, main_schedule_hash ) ); + trigger_block->producer_signature = main.get_private_key(config::system_account_name, "active").sign(sig_digest); + } + + // Push new trigger block to validator node. + // This should still fail because the NET bill is incorrect. + { + auto bs = validator.control->create_block_state_future( trigger_block ); + validator.control->abort_block(); + BOOST_REQUIRE_EXCEPTION( + validator.control->push_block( bs, forked_branch_callback{}, trx_meta_cache_lookup{} ), + fc::exception, fc_exception_message_is( "receipt does not match" ) + ); + } + + // Push new trigger block to validator2 node. + // Because validator2 is in light validation mode, this will not fail despite the fact that the NET bill is incorrect. + { + auto bs = validator2.control->create_block_state_future( trigger_block ); + validator2.control->abort_block(); + validator2.control->push_block( bs, forked_branch_callback{}, trx_meta_cache_lookup{} ); + } + } FC_LOG_AND_RETHROW() - BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END()