diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index c5d55772145..e8d961a39a5 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1275,8 +1275,8 @@ struct controller_impl { emit(self.applied_transaction, std::tie(trace, trx->packed_trx())); - if ( read_mode != db_read_mode::SPECULATIVE && pending->_block_status == controller::block_status::incomplete ) { - //this may happen automatically in destructor, but I prefere make it more explicit + if ( (read_mode != db_read_mode::SPECULATIVE && pending->_block_status == controller::block_status::incomplete) || trx->read_only ) { + //this may happen automatically in destructor, but I prefer to make it more explicit trx_context.undo(); } else { restore.cancel(); @@ -1696,7 +1696,7 @@ struct controller_impl { } else { packed_transaction_ptr ptrx( b, &pt ); // alias signed_block_ptr auto fut = transaction_metadata::start_recover_keys( - std::move( ptrx ), thread_pool.get_executor(), chain_id, microseconds::maximum() ); + std::move( ptrx ), thread_pool.get_executor(), chain_id, microseconds::maximum(), transaction_metadata::trx_type::input ); trx_metas.emplace_back( transaction_metadata_ptr{}, std::move( fut ) ); } } @@ -2735,8 +2735,8 @@ std::optional controller::pending_producer_block_id()const { } const deque& controller::get_pending_trx_receipts()const { - EOS_ASSERT( my->pending, block_validate_exception, "no pending block" ); - return my->pending->get_trx_receipts(); + static deque empty; + return my->pending ? my->pending->get_trx_receipts() : empty; } uint32_t controller::last_irreversible_block_num() const { diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index f65756a9289..8f279dba46a 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -373,7 +373,7 @@ namespace eosio { namespace chain { } template - fc::variant to_variant_with_abi( const T& obj, const abi_serializer::yield_function_t& yield ) { + fc::variant to_variant_with_abi( const T& obj, const abi_serializer::yield_function_t& yield ) const { fc::variant pretty_output; abi_serializer::to_variant( obj, pretty_output, [&]( account_name n ){ return get_abi_serializer( n, yield ); }, yield ); diff --git a/libraries/chain/include/eosio/chain/transaction_metadata.hpp b/libraries/chain/include/eosio/chain/transaction_metadata.hpp index 2508c602de3..934b10b9143 100644 --- a/libraries/chain/include/eosio/chain/transaction_metadata.hpp +++ b/libraries/chain/include/eosio/chain/transaction_metadata.hpp @@ -23,7 +23,8 @@ class transaction_metadata { enum class trx_type { input, implicit, - scheduled + scheduled, + read_only }; private: @@ -34,6 +35,7 @@ class transaction_metadata { public: const bool implicit; const bool scheduled; + const bool read_only; bool accepted = false; // not thread safe uint32_t billed_cpu_time_us = 0; // not thread safe @@ -53,12 +55,13 @@ class transaction_metadata { // creation of tranaction_metadata restricted to start_recover_keys and create_no_recover_keys below, public for make_shared explicit transaction_metadata( const private_type& pt, packed_transaction_ptr ptrx, fc::microseconds sig_cpu_usage, flat_set recovered_pub_keys, - bool _implicit = false, bool _scheduled = false) + bool _implicit = false, bool _scheduled = false, bool _read_only = false) : _packed_trx( std::move( ptrx ) ) , _sig_cpu_usage( sig_cpu_usage ) , _recovered_pub_keys( std::move( recovered_pub_keys ) ) , implicit( _implicit ) - , scheduled( _scheduled ) { + , scheduled( _scheduled ) + , read_only( _read_only ) { } transaction_metadata() = delete; @@ -79,13 +82,13 @@ class transaction_metadata { static recover_keys_future start_recover_keys( packed_transaction_ptr trx, boost::asio::io_context& thread_pool, const chain_id_type& chain_id, fc::microseconds time_limit, - uint32_t max_variable_sig_size = UINT32_MAX ); + trx_type t, uint32_t max_variable_sig_size = UINT32_MAX ); /// @returns constructed transaction_metadata with no key recovery (sig_cpu_usage=0, recovered_pub_keys=empty) static transaction_metadata_ptr create_no_recover_keys( packed_transaction_ptr trx, trx_type t ) { return std::make_shared( private_type(), std::move(trx), - fc::microseconds(), flat_set(), t == trx_type::implicit, t == trx_type::scheduled ); + fc::microseconds(), flat_set(), t == trx_type::implicit, t == trx_type::scheduled, t == trx_type::read_only ); } }; diff --git a/libraries/chain/transaction_metadata.cpp b/libraries/chain/transaction_metadata.cpp index d6d1861e50d..59f8a87791a 100644 --- a/libraries/chain/transaction_metadata.cpp +++ b/libraries/chain/transaction_metadata.cpp @@ -8,9 +8,10 @@ recover_keys_future transaction_metadata::start_recover_keys( packed_transaction boost::asio::io_context& thread_pool, const chain_id_type& chain_id, fc::microseconds time_limit, + trx_type t, uint32_t max_variable_sig_size ) { - return async_thread_pool( thread_pool, [trx{std::move(trx)}, chain_id, time_limit, max_variable_sig_size]() mutable { + return async_thread_pool( thread_pool, [trx{std::move(trx)}, chain_id, time_limit, t, max_variable_sig_size]() mutable { fc::time_point deadline = time_limit == fc::microseconds::maximum() ? fc::time_point::maximum() : fc::time_point::now() + time_limit; const vector& sigs = check_variable_sig_size( trx, max_variable_sig_size ); @@ -20,7 +21,8 @@ recover_keys_future transaction_metadata::start_recover_keys( packed_transaction const bool allow_duplicate_keys = false; fc::microseconds cpu_usage = trx->get_transaction().get_signature_keys(sigs, chain_id, deadline, *context_free_data, recovered_pub_keys, allow_duplicate_keys); - return std::make_shared( private_type(), std::move( trx ), cpu_usage, std::move( recovered_pub_keys ) ); + return std::make_shared( private_type(), std::move( trx ), cpu_usage, std::move( recovered_pub_keys ), + t == trx_type::implicit, t == trx_type::scheduled, t == trx_type::read_only); } ); } diff --git a/libraries/testing/tester.cpp b/libraries/testing/tester.cpp index cbce14de3ef..4fad0ebb6f3 100644 --- a/libraries/testing/tester.cpp +++ b/libraries/testing/tester.cpp @@ -575,7 +575,7 @@ namespace eosio { namespace testing { auto time_limit = deadline == fc::time_point::maximum() ? fc::microseconds::maximum() : fc::microseconds( deadline - fc::time_point::now() ); - auto fut = transaction_metadata::start_recover_keys( ptrx, control->get_thread_pool(), control->get_chain_id(), time_limit ); + auto fut = transaction_metadata::start_recover_keys( ptrx, control->get_thread_pool(), control->get_chain_id(), time_limit, transaction_metadata::trx_type::input ); auto r = control->push_transaction( fut.get(), deadline, billed_cpu_time_us, billed_cpu_time_us > 0, 0 ); if( r->except_ptr ) std::rethrow_exception( r->except_ptr ); if( r->except ) throw *r->except; @@ -600,7 +600,7 @@ namespace eosio { namespace testing { fc::microseconds::maximum() : fc::microseconds( deadline - fc::time_point::now() ); auto ptrx = std::make_shared( signed_transaction(trx), true, c ); - auto fut = transaction_metadata::start_recover_keys( std::move( ptrx ), control->get_thread_pool(), control->get_chain_id(), time_limit ); + auto fut = transaction_metadata::start_recover_keys( std::move( ptrx ), control->get_thread_pool(), control->get_chain_id(), time_limit, transaction_metadata::trx_type::input ); auto r = control->push_transaction( fut.get(), deadline, billed_cpu_time_us, billed_cpu_time_us > 0, 0 ); if (no_throw) return r; if( r->except_ptr ) std::rethrow_exception( r->except_ptr ); diff --git a/plugins/chain_api_plugin/chain_api_plugin.cpp b/plugins/chain_api_plugin/chain_api_plugin.cpp index 05296629af1..fd03f6e75b8 100644 --- a/plugins/chain_api_plugin/chain_api_plugin.cpp +++ b/plugins/chain_api_plugin/chain_api_plugin.cpp @@ -129,6 +129,7 @@ void chain_api_plugin::plugin_startup() { CHAIN_RO_CALL(abi_bin_to_json, 200, http_params_types::params_required), CHAIN_RO_CALL(get_required_keys, 200, http_params_types::params_required), CHAIN_RO_CALL(get_transaction_id, 200, http_params_types::params_required), + CHAIN_RO_CALL_ASYNC(push_ro_transaction, chain_apis::read_only::push_ro_transaction_results, 200, http_params_types::params_required), CHAIN_RW_CALL_ASYNC(push_block, chain_apis::read_write::push_block_results, 202, http_params_types::params_required), CHAIN_RW_CALL_ASYNC(push_transaction, chain_apis::read_write::push_transaction_results, 202, http_params_types::params_required), CHAIN_RW_CALL_ASYNC(push_transactions, chain_apis::read_write::push_transactions_results, 202, http_params_types::params_required), diff --git a/plugins/chain_interface/include/eosio/chain/plugin_interface.hpp b/plugins/chain_interface/include/eosio/chain/plugin_interface.hpp index bd5d950172b..b7233ba459c 100644 --- a/plugins/chain_interface/include/eosio/chain/plugin_interface.hpp +++ b/plugins/chain_interface/include/eosio/chain/plugin_interface.hpp @@ -43,10 +43,10 @@ namespace eosio { namespace chain { namespace plugin_interface { } namespace methods { - // synchronously push a block/trx to a single provider + // push a block/trx to a single provider using block_sync = method_decl&), first_provider_policy>; using blockvault_sync = method_decl; - using transaction_async = method_decl), first_provider_policy>; + using transaction_async = method_decl), first_provider_policy>; } } diff --git a/plugins/chain_plugin/chain_plugin.cpp b/plugins/chain_plugin/chain_plugin.cpp index a562612011d..3775dce5510 100644 --- a/plugins/chain_plugin/chain_plugin.cpp +++ b/plugins/chain_plugin/chain_plugin.cpp @@ -1380,7 +1380,7 @@ bool chain_plugin::accept_block(const signed_block_ptr& block, const block_id_ty } void chain_plugin::accept_transaction(const chain::packed_transaction_ptr& trx, next_function next) { - my->incoming_transaction_async_method(trx, false, std::move(next)); + my->incoming_transaction_async_method(trx, false, false, false, std::move(next)); } controller& chain_plugin::chain() { return *my->chain; } @@ -2768,7 +2768,7 @@ void read_write::push_transaction(const read_write::push_transaction_params& par fc_add_tag(trx_span, "trx_id", input_trx->id()); fc_add_tag(trx_span, "method", "push_transaction"); - app().get_method()(input_trx, true, + app().get_method()(input_trx, true, false, false, [this, token=trx_trace.get_token(), input_trx, next] (const std::variant& result) -> void { @@ -2911,7 +2911,7 @@ void read_write::send_transaction(const read_write::send_transaction_params& par fc_add_tag(trx_span, "trx_id", input_trx->id()); fc_add_tag(trx_span, "method", "send_transaction"); - app().get_method()(input_trx, true, + app().get_method()(input_trx, true, false, false, [this, token=trx_trace.get_token(), input_trx, next] (const std::variant& result) -> void { auto trx_span = fc_create_span_from_token(token, "Processed"); @@ -3183,6 +3183,75 @@ read_only::get_transaction_id_result read_only::get_transaction_id( const read_o return params.id(); } +void read_only::push_ro_transaction(const read_only::push_ro_transaction_params& params, chain::plugin_interface::next_function next) const { + try { + packed_transaction_v0 input_trx_v0; + auto resolver = make_resolver(db, abi_serializer::create_yield_function( abi_serializer_max_time )); + packed_transaction_ptr input_trx; + try { + abi_serializer::from_variant(params.transaction, input_trx_v0, std::move( resolver ), abi_serializer::create_yield_function( abi_serializer_max_time )); + input_trx = std::make_shared( std::move( input_trx_v0 ), true ); + } EOS_RETHROW_EXCEPTIONS(chain::packed_transaction_type_exception, "Invalid packed transaction") + + auto trx_trace = fc_create_trace_with_id("TransactionReadOnly", input_trx->id()); + auto trx_span = fc_create_span(trx_trace, "HTTP Received"); + fc_add_tag(trx_span, "trx_id", input_trx->id()); + fc_add_tag(trx_span, "method", "send_transaction"); + + app().get_method()(input_trx, true, true, static_cast(params.return_failure_traces), + [this, token=trx_trace.get_token(), input_trx, params, next] + (const std::variant& result) -> void { + auto trx_span = fc_create_span_from_token(token, "Processed"); + fc_add_tag(trx_span, "trx_id", input_trx->id()); + + if (std::holds_alternative(result)) { + auto& eptr = std::get(result); + fc_add_tag(trx_span, "error", eptr->to_string()); + next(eptr); + } else { + auto& trx_trace_ptr = std::get(result); + + fc_add_tag(trx_span, "block_num", trx_trace_ptr->block_num); + fc_add_tag(trx_span, "block_time", trx_trace_ptr->block_time.to_time_point()); + fc_add_tag(trx_span, "elapsed", trx_trace_ptr->elapsed.count()); + if( trx_trace_ptr->receipt ) { + fc_add_tag(trx_span, "status", std::string(trx_trace_ptr->receipt->status)); + } + if( trx_trace_ptr->except ) { + fc_add_tag(trx_span, "error", trx_trace_ptr->except->to_string()); + } + + try { + fc::variant output; + try { + output = db.to_variant_with_abi( *trx_trace_ptr, abi_serializer::create_yield_function( abi_serializer_max_time ) ); + } catch( chain::abi_exception& ) { + output = *trx_trace_ptr; + } + const auto& accnt_metadata_obj = db.db().get( params.account_name ); + const auto& receipts = db.get_pending_trx_receipts(); + vector pending_transactions; + pending_transactions.reserve(receipts.size()); + for( transaction_receipt const& receipt : receipts ) { + if( std::holds_alternative(receipt.trx) ) { + pending_transactions.push_back(std::get(receipt.trx)); + } + else { + pending_transactions.push_back(std::get(receipt.trx).id()); + } + } + next(read_only::push_ro_transaction_results{db.head_block_num(), db.head_block_id(), db.last_irreversible_block_num(), db.last_irreversible_block_id(), + accnt_metadata_obj.code_hash, pending_transactions, output}); + } CATCH_AND_CALL(next); + } + }); + } catch ( boost::interprocess::bad_alloc& ) { + chain_plugin::handle_db_exhaustion(); + } catch ( const std::bad_alloc& ) { + chain_plugin::handle_bad_alloc(); + } CATCH_AND_CALL(next); +} + account_query_db::get_accounts_by_authorizers_result read_only::get_accounts_by_authorizers( const account_query_db::get_accounts_by_authorizers_params& args) const { EOS_ASSERT(aqdb.has_value(), plugin_config_exception, "Account Queries being accessed when not enabled"); diff --git a/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp b/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp index 18a79cd7cbe..1e85b357527 100644 --- a/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp +++ b/plugins/chain_plugin/include/eosio/chain_plugin/chain_plugin.hpp @@ -565,6 +565,24 @@ class read_only { }; } + struct push_ro_transaction_params { + name account_name; + bool return_failure_traces = false; + fc::variant transaction; + }; + + struct push_ro_transaction_results { + uint32_t head_block_num = 0; + chain::block_id_type head_block_id; + uint32_t last_irreversible_block_num = 0; + chain::block_id_type last_irreversible_block_id; + digest_type code_hash; + vector pending_transactions; + fc::variant result; + }; + + void push_ro_transaction(const push_ro_transaction_params& params, chain::plugin_interface::next_function next ) const; + template static void copy_inline_row(const KeyValueObj& obj, vector& data) { data.resize( obj.value.size() ); @@ -1123,3 +1141,6 @@ FC_REFLECT( eosio::chain_apis::read_only::abi_bin_to_json_params, (code)(action) FC_REFLECT( eosio::chain_apis::read_only::abi_bin_to_json_result, (args) ) FC_REFLECT( eosio::chain_apis::read_only::get_required_keys_params, (transaction)(available_keys) ) FC_REFLECT( eosio::chain_apis::read_only::get_required_keys_result, (required_keys) ) +FC_REFLECT( eosio::chain_apis::read_only::push_ro_transaction_params, (account_name)(return_failure_traces)(transaction) ) +FC_REFLECT( eosio::chain_apis::read_only::push_ro_transaction_results, (head_block_num)(head_block_id)(last_irreversible_block_num)(last_irreversible_block_id)(code_hash)(pending_transactions)(result) ) + diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index 3e6f39768dd..261cda921d2 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -445,19 +445,25 @@ class producer_plugin_impl : public std::enable_shared_from_this next) { + void on_incoming_transaction_async(const packed_transaction_ptr& trx, + bool persist_until_expired, + const bool read_only, + const bool return_failure_trace, + next_function next) { chain::controller& chain = chain_plug->chain(); const auto max_trx_time_ms = _max_transaction_time_ms.load(); fc::microseconds max_trx_cpu_usage = max_trx_time_ms < 0 ? fc::microseconds::maximum() : fc::milliseconds( max_trx_time_ms ); auto future = transaction_metadata::start_recover_keys( trx, _thread_pool->get_executor(), - chain.get_chain_id(), fc::microseconds( max_trx_cpu_usage ), chain.configured_subjective_signature_length_limit() ); + chain.get_chain_id(), fc::microseconds( max_trx_cpu_usage ), + read_only ? transaction_metadata::trx_type::read_only : transaction_metadata::trx_type::input, + chain.configured_subjective_signature_length_limit() ); - boost::asio::post(_thread_pool->get_executor(), [self = this, future{std::move(future)}, persist_until_expired, + boost::asio::post(_thread_pool->get_executor(), [self = this, future{std::move(future)}, persist_until_expired, return_failure_trace, 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{std::move(trx)}]() mutable { + app().post( priority::low, [self, future{std::move(future)}, persist_until_expired, next{std::move( next )}, trx{std::move(trx)}, return_failure_trace]() mutable { auto exception_handler = [self, &next, trx{std::move(trx)}](fc::exception_ptr ex) { 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())); @@ -470,7 +476,7 @@ class producer_plugin_impl : public std::enable_shared_from_thisprocess_incoming_transaction_async( result, persist_until_expired, next ) ) { + if( !self->process_incoming_transaction_async( result, persist_until_expired, next, return_failure_trace ) ) { if( self->_pending_block_mode == pending_block_mode::producing ) { self->schedule_maybe_produce_block( true ); } else { @@ -483,7 +489,10 @@ class producer_plugin_impl : public std::enable_shared_from_this next) { + bool process_incoming_transaction_async(const transaction_metadata_ptr& trx, + bool persist_until_expired, + next_function next, + const bool return_failure_trace = false) { bool exhausted = false; chain::controller& chain = chain_plug->chain(); @@ -491,7 +500,9 @@ class producer_plugin_impl : public std::enable_shared_from_this(response)) { - _transaction_ack_channel.publish(priority::low, std::pair(std::get(response), trx)); + if (!trx->read_only) { + _transaction_ack_channel.publish(priority::low, std::pair(std::get(response), trx)); + } if (_pending_block_mode == pending_block_mode::producing) { 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) @@ -519,8 +530,9 @@ class producer_plugin_impl : public std::enable_shared_from_this(nullptr, trx)); + if (!trx->read_only) { + _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}, auth: ${a}", @@ -612,8 +624,12 @@ class producer_plugin_impl : public std::enable_shared_from_thiselapsed, fc::time_point::now() ); - auto e_ptr = trace->except->dynamic_copy_exception(); - send_response( e_ptr ); + if( return_failure_trace ) { + send_response( trace ); + } else { + auto e_ptr = trace->except->dynamic_copy_exception(); + send_response( e_ptr ); + } } } else { if( persist_until_expired && !_disable_persist_until_expired ) { @@ -955,7 +971,7 @@ void producer_plugin::plugin_initialize(const boost::program_options::variables_ my->_incoming_transaction_subscription = app().get_channel().subscribe( [this](const packed_transaction_ptr& trx) { try { - my->on_incoming_transaction_async(trx, false, [](const auto&){}); + my->on_incoming_transaction_async(trx, false, false, false, [](const auto&){}); } LOG_AND_DROP(); }); @@ -970,8 +986,8 @@ void producer_plugin::plugin_initialize(const boost::program_options::variables_ }); my->_incoming_transaction_async_provider = app().get_method().register_provider( - [this](const packed_transaction_ptr& trx, bool persist_until_expired, next_function next) -> void { - return my->on_incoming_transaction_async(trx, persist_until_expired, next ); + [this](const packed_transaction_ptr& trx, bool persist_until_expired, const bool read_only, const bool return_failure_trace, next_function next) -> void { + return my->on_incoming_transaction_async(trx, persist_until_expired, read_only, return_failure_trace, next ); }); if (options.count("greylist-account")) { diff --git a/programs/cleos/httpc.hpp b/programs/cleos/httpc.hpp index 575fde0fb18..7c33fb44972 100644 --- a/programs/cleos/httpc.hpp +++ b/programs/cleos/httpc.hpp @@ -83,6 +83,7 @@ namespace eosio { namespace client { namespace http { const string send_txn_func = chain_func_base + "/send_transaction"; const string push_txn_func = chain_func_base + "/push_transaction"; const string push_txns_func = chain_func_base + "/push_transactions"; + const string push_ro_txns_func = chain_func_base + "/push_ro_transaction"; const string json_to_bin_func = chain_func_base + "/abi_json_to_bin"; const string get_block_func = chain_func_base + "/get_block"; const string get_block_info_func = chain_func_base + "/get_block_info"; @@ -102,6 +103,7 @@ namespace eosio { namespace client { namespace http { const string get_schedule_func = chain_func_base + "/get_producer_schedule"; const string get_required_keys = chain_func_base + "/get_required_keys"; + const string history_func_base = "/v1/history"; const string get_actions_func = history_func_base + "/get_actions"; const string get_transaction_func = history_func_base + "/get_transaction"; diff --git a/programs/cleos/main.cpp b/programs/cleos/main.cpp index f2fd1e1f13c..ba86b622f33 100644 --- a/programs/cleos/main.cpp +++ b/programs/cleos/main.cpp @@ -180,6 +180,9 @@ bool tx_dont_broadcast = false; bool tx_return_packed = false; bool tx_skip_sign = false; bool tx_print_json = false; +bool tx_ro_print_json = false; +bool tx_rtn_failure_trace = false; +bool tx_read_only = false; bool tx_use_old_rpc = false; string tx_json_save_file; bool print_request = false; @@ -444,7 +447,19 @@ fc::variant push_transaction( signed_transaction& trx, const std::vector 0 ? trx.actions[0].account : ""_n; + auto args = fc::mutable_variant_object() + ("account_name", account_name) + ("return_failure_traces", tx_rtn_failure_trace) + ("transaction", pt_v0); + return call(push_ro_txns_func, args); + }else { + EOSC_ASSERT( !tx_rtn_failure_trace, "ERROR: --return-failure-trace can only be used along with --read-only" ); + return call(send_txn_func, packed_transaction_v0(trx, compression)); + } } catch (chain::missing_chain_api_plugin_exception &) { std::cerr << "New RPC send_transaction may not be supported. Add flag --use-old-rpc to use old RPC push_transaction instead." << std::endl; @@ -636,7 +651,10 @@ void send_actions(std::vector&& actions, const std::vectoradd_subcommand("servants", localized("Retrieve accounts which are servants of a given account ")); @@ -3881,6 +3898,8 @@ int main( int argc, char** argv ) { auto trxSubcommand = push->add_subcommand("transaction", localized("Push an arbitrary JSON transaction")); trxSubcommand->add_option("transaction", trx_to_push, localized("The JSON string or filename defining the transaction to push"))->required(); add_standard_transaction_options_plus_signing(trxSubcommand); + trxSubcommand->add_flag("-o,--read-only", tx_read_only, localized("Specify a transaction is read-only")); + trxSubcommand->add_flag("-t,--return-failure-trace", tx_rtn_failure_trace, localized("Return partial traces on failed transactions, use it along with --read-only)")); trxSubcommand->callback([&] { fc::variant trx_var = json_from_file_or_string(trx_to_push); @@ -3905,7 +3924,6 @@ int main( int argc, char** argv ) { std::cout << fc::json::to_pretty_string(trxs_result) << std::endl; }); - // multisig subcommand auto msig = app.add_subcommand("multisig", localized("Multisig contract commands")); msig->require_subcommand(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 92d32a8fc3b..172c5291967 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -69,6 +69,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/privacy_simple_network.py ${CMAKE_CUR configure_file(${CMAKE_CURRENT_SOURCE_DIR}/privacy_tls_test.py ${CMAKE_CURRENT_BINARY_DIR}/privacy_tls_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/privacy_scenario_3_test.py ${CMAKE_CURRENT_BINARY_DIR}/privacy_scenario_3_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/privacy_forked_network.py ${CMAKE_CURRENT_BINARY_DIR}/privacy_forked_network.py COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/read_only_query_tests.py ${CMAKE_CURRENT_BINARY_DIR}/read_only_query_tests.py COPYONLY) #To run plugin_test with all log from blockchain displayed, put --verbose after --, i.e. plugin_test -- --verbose add_test(NAME plugin_test COMMAND plugin_test --report_level=detailed --color_output) @@ -135,6 +136,8 @@ add_test(NAME privacy_scenario_3_test COMMAND tests/privacy_scenario_3_test.py - set_property(TEST privacy_scenario_3_test PROPERTY LABELS nonparallelizable_tests) add_test(NAME privacy_forked_network COMMAND tests/privacy_forked_network.py -p 4 -v --dump-error-detail WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_property(TEST privacy_forked_network PROPERTY LABELS nonparallelizable_tests) +add_test(NAME read_only_query COMMAND tests/read_only_query_tests.py -p 1 -v --dump-error-detail WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +set_property(TEST read_only_query PROPERTY LABELS nonparallelizable_tests) # Long running tests add_test(NAME nodeos_sanity_lr_test COMMAND tests/nodeos_run_test.py -v --sanity-test --clean-run --dump-error-detail WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/tests/read_only_query_tests.py b/tests/read_only_query_tests.py new file mode 100755 index 00000000000..eaa33510d33 --- /dev/null +++ b/tests/read_only_query_tests.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 + +from testUtils import Account +from testUtils import Utils +from testUtils import ReturnType +from Cluster import Cluster +from WalletMgr import WalletMgr +from TestHelper import TestHelper + +import random + +############################################################### +# read_only_query_tests +# +# Loads read-only query test contract and validates +# new API disallows write operations. +# Also validates retrieval from multiple KV tables in a single query. +# +############################################################### + +Print=Utils.Print +errorExit=Utils.errorExit + +args=TestHelper.parse_args({"-p","-n","-d","-s","--nodes-file","--seed" + ,"--dump-error-details","-v","--leave-running" + ,"--clean-run","--keep-logs"}) + +pnodes=args.p +topo=args.s +delay=args.d +total_nodes = pnodes if args.n < pnodes else args.n +debug=args.v +nodesFile=args.nodes_file +dontLaunch=nodesFile is not None +seed=args.seed +dontKill=args.leave_running +dumpErrorDetails=args.dump_error_details +killAll=args.clean_run +keepLogs=args.keep_logs + +killWallet=not dontKill +killEosInstances=not dontKill +if nodesFile is not None: + killEosInstances=False + +Utils.Debug=debug +testSuccessful=False + +random.seed(seed) # Use a fixed seed for repeatability. +cluster=Cluster(walletd=True) + +walletMgr=WalletMgr(True) +EOSIO_ACCT_PRIVATE_DEFAULT_KEY = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +EOSIO_ACCT_PUBLIC_DEFAULT_KEY = "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" +contractDir='unittests/contracts' +kvWasmFile='kv_bios.wasm' +kvAbiFile='kv_bios.abi' +wasmFile='read_only_query_tests.wasm' +abiFile='read_only_query_tests.abi' + +try: + if dontLaunch: # run test against remote cluster + jsonStr=None + with open(nodesFile, "r") as f: + jsonStr=f.read() + if not cluster.initializeNodesFromJson(jsonStr): + errorExit("Failed to initilize nodes from Json string.") + total_nodes=len(cluster.getNodes()) + + walletMgr.killall(allInstances=killAll) + walletMgr.cleanup() + print("Stand up walletd") + if walletMgr.launch() is False: + errorExit("Failed to stand up keosd.") + else: + cluster.killall(allInstances=killAll) + cluster.cleanup() + + Print ("producing nodes: %s, non-producing nodes: %d, topology: %s, delay between nodes launch(seconds): %d" % + (pnodes, total_nodes-pnodes, topo, delay)) + + Print("Stand up cluster") + if cluster.launch(pnodes=pnodes, totalNodes=total_nodes, topo=topo, delay=delay) is False: + errorExit("Failed to stand up eos cluster.") + + Print ("Wait for Cluster stabilization") + # wait for cluster to start producing blocks + if not cluster.waitOnClusterBlockNumSync(3): + errorExit("Cluster never stabilized") + + Print("Creating kv settings account") + kvsettingsaccount = Account('kvsettings11') + kvsettingsaccount.ownerPublicKey = EOSIO_ACCT_PUBLIC_DEFAULT_KEY + kvsettingsaccount.activePublicKey = EOSIO_ACCT_PUBLIC_DEFAULT_KEY + cluster.createAccountAndVerify(kvsettingsaccount, cluster.eosioAccount, buyRAM=700000) + Print("Creating read-only query account") + readtestaccount = Account('readtest1111') + readtestaccount.ownerPublicKey = EOSIO_ACCT_PUBLIC_DEFAULT_KEY + readtestaccount.activePublicKey = EOSIO_ACCT_PUBLIC_DEFAULT_KEY + cluster.createAccountAndVerify(readtestaccount, cluster.eosioAccount, buyRAM=700000) + + Print("Validating accounts after bootstrap") + cluster.validateAccounts([kvsettingsaccount, readtestaccount]) + + node = cluster.getNode() + + Print("Setting KV settings account privileged") + node.pushMessage(cluster.eosioAccount.name, 'setpriv', '["kvsettings11", 1]', '-p eosio@active') + Print("Loading KV bios") + node.publishContract(kvsettingsaccount, contractDir, kvWasmFile, kvAbiFile, waitForTransBlock=True) + node.pushMessage(kvsettingsaccount.name, 'ramkvlimits', '[1024, 1024, 1024]', '-p kvsettings11@active') + node.publishContract(readtestaccount, contractDir, wasmFile, abiFile, waitForTransBlock=True) + + Print("Running setup as read-only query") + trx = { + "actions": [{"account": readtestaccount.name, "name": "setup", + "authorization": [{"actor": readtestaccount.name, "permission": "active"}], + "data": ""}] + } + node.pushTransaction(trx, opts='--read-only', permissions=readtestaccount.name) + + Print("Verifying kv tables not written") + cmd="get kv_table %s roqm id" % readtestaccount.name + transaction = node.processCleosCmd(cmd, cmd, False, returnType=ReturnType.raw) + expected = '''{ + "rows": [], + "more": false, + "next_key": "", + "next_key_bytes": "" +} +''' + assert expected == transaction, 'kv table roqm should be empty' + + Print("Setting up read-only tables") + success, transaction = node.pushMessage(readtestaccount.name, 'setup', "{}", '-p readtest1111@active') + + Print("Querying combined kv tables") + trx = { + "actions": [{"account": readtestaccount.name, "name": "get", + "authorization": [{"actor": readtestaccount.name, "permission": "active"}], + "data": ""}] + } + success, transaction = node.pushTransaction(trx, opts='--read-only --return-failure-trace', permissions=readtestaccount.name) + + assert success + assert 8 == len(transaction['result']['action_traces'][0]['return_value_data']), 'Combined kv tables roqm and roqf should contain 8 rows' + + testSuccessful=True +finally: + TestHelper.shutdown(cluster, walletMgr, testSuccessful, killEosInstances, killWallet, keepLogs, killAll, dumpErrorDetails) + +exit(0) diff --git a/unittests/api_tests.cpp b/unittests/api_tests.cpp index eb6b87f1798..1634716ef06 100644 --- a/unittests/api_tests.cpp +++ b/unittests/api_tests.cpp @@ -1308,7 +1308,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(transaction_tests, TESTER_T, backing_store_ts) { t BOOST_CHECK(pkt.get_packed_transaction() == packed); ptrx = std::make_shared( pkt, true ); - auto fut = transaction_metadata::start_recover_keys( std::move( ptrx ), t.control->get_thread_pool(), t.control->get_chain_id(), time_limit ); + auto fut = transaction_metadata::start_recover_keys( std::move( ptrx ), t.control->get_thread_pool(), t.control->get_chain_id(), time_limit, transaction_metadata::trx_type::input ); auto r = t.control->push_transaction( fut.get(), fc::time_point::maximum(), t.DEFAULT_BILLED_CPU_TIME_US, true, 0 ); if( r->except_ptr ) std::rethrow_exception( r->except_ptr ); if( r->except) throw *r->except; diff --git a/unittests/contracts/CMakeLists.txt b/unittests/contracts/CMakeLists.txt index 1c1c3003901..63f993cea3c 100644 --- a/unittests/contracts/CMakeLists.txt +++ b/unittests/contracts/CMakeLists.txt @@ -9,3 +9,8 @@ file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/eosio.wrap/ DESTINATION ${CMAKE_CURRENT_BI file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/old_versions/v1.6.0-rc3/eosio.bios/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/old_versions/v1.6.0-rc3/eosio.bios/) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/old_versions/v1.7.0-develop-preactivate_feature/eosio.bios/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/old_versions/v1.7.0-develop-preactivate_feature/eosio.bios/) + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/kv_bios.abi DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/kv_bios.wasm DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/read_only_query_tests.abi DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/read_only_query_tests.wasm DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) diff --git a/unittests/contracts/kv_bios.abi b/unittests/contracts/kv_bios.abi new file mode 100644 index 00000000000..c7cc890870e --- /dev/null +++ b/unittests/contracts/kv_bios.abi @@ -0,0 +1,96 @@ +{ + "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", + "version": "eosio::abi/1.1", + "types": [], + "structs": [ + { + "name": "diskkvlimits", + "base": "", + "fields": [ + { + "name": "k", + "type": "uint32" + }, + { + "name": "v", + "type": "uint32" + }, + { + "name": "i", + "type": "uint32" + } + ] + }, + { + "name": "ramkvlimits", + "base": "", + "fields": [ + { + "name": "k", + "type": "uint32" + }, + { + "name": "v", + "type": "uint32" + }, + { + "name": "i", + "type": "uint32" + } + ] + }, + { + "name": "setdisklimit", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "limit", + "type": "int64" + } + ] + }, + { + "name": "setramlimit", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "limit", + "type": "int64" + } + ] + } + ], + "actions": [ + { + "name": "diskkvlimits", + "type": "diskkvlimits", + "ricardian_contract": "" + }, + { + "name": "ramkvlimits", + "type": "ramkvlimits", + "ricardian_contract": "" + }, + { + "name": "setdisklimit", + "type": "setdisklimit", + "ricardian_contract": "" + }, + { + "name": "setramlimit", + "type": "setramlimit", + "ricardian_contract": "" + } + ], + "tables": [], + "ricardian_clauses": [], + "variants": [] +} \ No newline at end of file diff --git a/unittests/contracts/kv_bios.wasm b/unittests/contracts/kv_bios.wasm new file mode 100755 index 00000000000..e17ea7f396d Binary files /dev/null and b/unittests/contracts/kv_bios.wasm differ diff --git a/unittests/contracts/read_only_query_tests.abi b/unittests/contracts/read_only_query_tests.abi new file mode 100644 index 00000000000..a19665abf9a --- /dev/null +++ b/unittests/contracts/read_only_query_tests.abi @@ -0,0 +1,105 @@ +{ + "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", + "version": "eosio::abi/1.2", + "types": [], + "structs": [ + { + "name": "get", + "base": "", + "fields": [] + }, + { + "name": "my_struct", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint32" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "gender", + "type": "uint32" + }, + { + "name": "age", + "type": "uint32" + } + ] + }, + { + "name": "put", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint32" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "gender", + "type": "uint32" + }, + { + "name": "age", + "type": "uint32" + } + ] + }, + { + "name": "setup", + "base": "", + "fields": [] + } + ], + "actions": [ + { + "name": "get", + "type": "get", + "ricardian_contract": "" + }, + { + "name": "put", + "type": "put", + "ricardian_contract": "" + }, + { + "name": "setup", + "type": "setup", + "ricardian_contract": "" + } + ], + "tables": [], + "kv_tables": { + "roqf": { + "type": "my_struct", + "primary_index": { + "name": "id", + "type": "uint32" + }, + "secondary_indices": {} + }, + "roqm": { + "type": "my_struct", + "primary_index": { + "name": "id", + "type": "uint32" + }, + "secondary_indices": {} + } + }, + "ricardian_clauses": [], + "variants": [], + "action_results": [ + { + "name": "get", + "result_type": "my_struct[]" + } + ] +} \ No newline at end of file diff --git a/unittests/contracts/read_only_query_tests.wasm b/unittests/contracts/read_only_query_tests.wasm new file mode 100755 index 00000000000..8209479e7f8 Binary files /dev/null and b/unittests/contracts/read_only_query_tests.wasm differ diff --git a/unittests/misc_tests.cpp b/unittests/misc_tests.cpp index 66234af49ec..40c7711f0e9 100644 --- a/unittests/misc_tests.cpp +++ b/unittests/misc_tests.cpp @@ -963,12 +963,12 @@ BOOST_AUTO_TEST_CASE(transaction_metadata_test) { try { named_thread_pool thread_pool( "misc", 5 ); - auto fut = transaction_metadata::start_recover_keys( ptrx, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum() ); - auto fut2 = transaction_metadata::start_recover_keys( ptrx2, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum() ); + auto fut = transaction_metadata::start_recover_keys( ptrx, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum(), transaction_metadata::trx_type::input ); + auto fut2 = transaction_metadata::start_recover_keys( ptrx2, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum(), transaction_metadata::trx_type::input ); // start another key reovery on same packed_transaction, creates a new future with transaction_metadata, should not interfere with above - transaction_metadata::start_recover_keys( ptrx, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum() ); - transaction_metadata::start_recover_keys( ptrx2, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum() ); + transaction_metadata::start_recover_keys( ptrx, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum(), transaction_metadata::trx_type::input ); + transaction_metadata::start_recover_keys( ptrx2, thread_pool.get_executor(), test.control->get_chain_id(), fc::microseconds::maximum(), transaction_metadata::trx_type::input ); auto mtrx = fut.get(); const auto& keys = mtrx->recovered_keys(); diff --git a/unittests/wasm_tests.cpp b/unittests/wasm_tests.cpp index 9391020c9a6..2f5be2b793a 100644 --- a/unittests/wasm_tests.cpp +++ b/unittests/wasm_tests.cpp @@ -1898,7 +1898,7 @@ BOOST_AUTO_TEST_CASE( billed_cpu_test ) try { trx.max_cpu_usage_ms = trx_max_ms; trx.sign( chain.get_private_key( acc, "active" ), chain.control->get_chain_id() ); auto ptrx = std::make_shared(std::move(trx), true); - auto fut = transaction_metadata::start_recover_keys( ptrx, chain.control->get_thread_pool(), chain.control->get_chain_id(), fc::microseconds::maximum() ); + auto fut = transaction_metadata::start_recover_keys( ptrx, chain.control->get_thread_pool(), chain.control->get_chain_id(), fc::microseconds::maximum(), transaction_metadata::trx_type::input ); return fut.get(); };