diff --git a/libraries/chain/apply_context.cpp b/libraries/chain/apply_context.cpp index ba1f2b2d521..4eefb4824b3 100644 --- a/libraries/chain/apply_context.cpp +++ b/libraries/chain/apply_context.cpp @@ -306,13 +306,45 @@ void apply_context::execute_context_free_inline( action&& a ) { void apply_context::schedule_deferred_transaction( const uint128_t& sender_id, account_name payer, transaction&& trx, bool replace_existing ) { EOS_ASSERT( trx.context_free_actions.size() == 0, cfa_inside_generated_tx, "context free actions are not currently allowed in generated transactions" ); - trx.expiration = control.pending_block_time() + fc::microseconds(999'999); // Rounds up to nearest second (makes expiration check unnecessary) - trx.set_reference_block(control.head_block_id()); // No TaPoS check necessary bool enforce_actor_whitelist_blacklist = trx_context.enforce_whiteblacklist && control.is_producing_block() && !control.sender_avoids_whitelist_blacklist_enforcement( receiver ); trx_context.validate_referenced_accounts( trx, enforce_actor_whitelist_blacklist ); + if( control.is_builtin_activated( builtin_protocol_feature_t::no_duplicate_deferred_id ) ) { + auto exts = trx.validate_and_extract_extensions(); + if( exts.size() > 0 ) { + EOS_ASSERT( exts.size() == 1, invalid_transaction_extension, + "only one extension is currently supported for deferred transactions" + ); + const auto& context = exts.front().get(); + EOS_ASSERT( context.sender == receiver, ill_formed_deferred_transaction_generation_context, + "deferred transaction generaction context contains mismatching sender", + ("expected", receiver)("actual", context.sender) + ); + EOS_ASSERT( context.sender_id == sender_id, ill_formed_deferred_transaction_generation_context, + "deferred transaction generaction context contains mismatching sender_id", + ("expected", sender_id)("actual", context.sender_id) + ); + EOS_ASSERT( context.sender_trx_id == trx_context.id, ill_formed_deferred_transaction_generation_context, + "deferred transaction generaction context contains mismatching sender_trx_id", + ("expected", trx_context.id)("actual", context.sender_trx_id) + ); + } else { + FC_ASSERT( trx.transaction_extensions.size() == 0, "invariant failure" ); + trx.transaction_extensions.emplace_back( + deferred_transaction_generation_context::extension_id(), + fc::raw::pack( deferred_transaction_generation_context( trx_context.id, sender_id, receiver ) ) + ); + } + trx.expiration = time_point_sec(); + trx.ref_block_num = 0; + trx.ref_block_prefix = 0; + } else { + trx.expiration = control.pending_block_time() + fc::microseconds(999'999); // Rounds up to nearest second (makes expiration check unnecessary) + trx.set_reference_block(control.head_block_id()); // No TaPoS check necessary + } + // Charge ahead of time for the additional net usage needed to retire the deferred transaction // whether that be by successfully executing, soft failure, hard failure, or expiration. const auto& cfg = control.get_global_properties().configuration; diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index c3ee723308b..6e543c8a356 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -956,8 +956,14 @@ struct controller_impl { // Deliver onerror action containing the failed deferred transaction directly back to the sender. etrx.actions.emplace_back( vector{{gtrx.sender, config::active_name}}, onerror( gtrx.sender_id, gtrx.packed_trx.data(), gtrx.packed_trx.size() ) ); - etrx.expiration = self.pending_block_time() + fc::microseconds(999'999); // Round up to avoid appearing expired - etrx.set_reference_block( self.head_block_id() ); + if( self.is_builtin_activated( builtin_protocol_feature_t::no_duplicate_deferred_id ) ) { + etrx.expiration = time_point_sec(); + etrx.ref_block_num = 0; + etrx.ref_block_prefix = 0; + } else { + etrx.expiration = self.pending_block_time() + fc::microseconds(999'999); // Round up to nearest second to avoid appearing expired + etrx.set_reference_block( self.head_block_id() ); + } transaction_context trx_context( self, etrx, etrx.id(), start ); trx_context.deadline = deadline; @@ -979,6 +985,8 @@ struct controller_impl { trx_context.squash(); restore.cancel(); return trace; + } catch( const disallowed_transaction_extensions_bad_block_exception& ) { + throw; } catch( const protocol_feature_bad_block_exception& ) { throw; } catch( const fc::exception& e ) { @@ -1121,6 +1129,8 @@ struct controller_impl { restore.cancel(); return trace; + } catch( const disallowed_transaction_extensions_bad_block_exception& ) { + throw; } catch( const protocol_feature_bad_block_exception& ) { throw; } catch( const fc::exception& e ) { @@ -1314,6 +1324,10 @@ struct controller_impl { unapplied_transactions.erase( trx->signed_id ); } return trace; + } catch( const disallowed_transaction_extensions_bad_block_exception& ) { + throw; + } catch( const protocol_feature_bad_block_exception& ) { + throw; } catch (const fc::exception& e) { trace->except = e; trace->except_ptr = std::current_exception(); @@ -2150,8 +2164,14 @@ struct controller_impl { signed_transaction trx; trx.actions.emplace_back(std::move(on_block_act)); - trx.set_reference_block(self.head_block_id()); - trx.expiration = self.pending_block_time() + fc::microseconds(999'999); // Round up to nearest second to avoid appearing expired + if( self.is_builtin_activated( builtin_protocol_feature_t::no_duplicate_deferred_id ) ) { + trx.expiration = time_point_sec(); + trx.ref_block_num = 0; + trx.ref_block_prefix = 0; + } else { + trx.expiration = self.pending_block_time() + fc::microseconds(999'999); // Round up to nearest second to avoid appearing expired + trx.set_reference_block( self.head_block_id() ); + } return trx; } diff --git a/libraries/chain/include/eosio/chain/block_header.hpp b/libraries/chain/include/eosio/chain/block_header.hpp index adbdb7d3def..fc751826d95 100644 --- a/libraries/chain/include/eosio/chain/block_header.hpp +++ b/libraries/chain/include/eosio/chain/block_header.hpp @@ -8,41 +8,6 @@ namespace eosio { namespace chain { namespace detail { - struct extract_match { - bool enforce_unique = false; - }; - - template - struct decompose; - - template<> - struct decompose<> { - template - static auto extract( uint16_t id, const vector& data, ResultVariant& result ) - -> fc::optional - { - return {}; - } - }; - - template - struct decompose { - using head_t = T; - using tail_t = decompose< Rest... >; - - template - static auto extract( uint16_t id, const vector& data, ResultVariant& result ) - -> fc::optional - { - if( id == head_t::extension_id() ) { - result = fc::raw::unpack( data ); - return { extract_match{ head_t::enforce_unique() } }; - } - - return tail_t::template extract( id, data, result ); - } - }; - template struct block_header_extension_types { using block_header_extensions_t = fc::static_variant< Ts... >; diff --git a/libraries/chain/include/eosio/chain/exceptions.hpp b/libraries/chain/include/eosio/chain/exceptions.hpp index a80213e0425..2976dcbb7f1 100644 --- a/libraries/chain/include/eosio/chain/exceptions.hpp +++ b/libraries/chain/include/eosio/chain/exceptions.hpp @@ -196,6 +196,12 @@ namespace eosio { namespace chain { 3040013, "Transaction is too big" ) FC_DECLARE_DERIVED_EXCEPTION( unknown_transaction_compression, transaction_exception, 3040014, "Unknown transaction compression" ) + FC_DECLARE_DERIVED_EXCEPTION( invalid_transaction_extension, transaction_exception, + 3040015, "Invalid transaction extension" ) + FC_DECLARE_DERIVED_EXCEPTION( ill_formed_deferred_transaction_generation_context, transaction_exception, + 3040016, "Transaction includes an ill-formed deferred transaction generation context extension" ) + FC_DECLARE_DERIVED_EXCEPTION( disallowed_transaction_extensions_bad_block_exception, transaction_exception, + 3250002, "Transaction includes disallowed extensions (invalid block)" ) FC_DECLARE_DERIVED_EXCEPTION( action_validate_exception, chain_exception, diff --git a/libraries/chain/include/eosio/chain/protocol_feature_manager.hpp b/libraries/chain/include/eosio/chain/protocol_feature_manager.hpp index 097b8772d1a..118709a9b17 100644 --- a/libraries/chain/include/eosio/chain/protocol_feature_manager.hpp +++ b/libraries/chain/include/eosio/chain/protocol_feature_manager.hpp @@ -17,6 +17,7 @@ enum class builtin_protocol_feature_t : uint32_t { preactivate_feature, only_link_to_existing_permission, replace_deferred, + no_duplicate_deferred_id, fix_linkauth_restriction, disallow_empty_producer_schedule, restrict_action_to_self, diff --git a/libraries/chain/include/eosio/chain/transaction.hpp b/libraries/chain/include/eosio/chain/transaction.hpp index 54ea0869730..8f103eb6601 100644 --- a/libraries/chain/include/eosio/chain/transaction.hpp +++ b/libraries/chain/include/eosio/chain/transaction.hpp @@ -9,6 +9,39 @@ namespace eosio { namespace chain { + struct deferred_transaction_generation_context : fc::reflect_init { + static constexpr uint16_t extension_id() { return 0; } + static constexpr bool enforce_unique() { return true; } + + deferred_transaction_generation_context() = default; + + deferred_transaction_generation_context( const transaction_id_type& sender_trx_id, uint128_t sender_id, account_name sender ) + :sender_trx_id( sender_trx_id ) + ,sender_id( sender_id ) + ,sender( sender ) + {} + + void reflector_init(); + + transaction_id_type sender_trx_id; + uint128_t sender_id; + account_name sender; + }; + + namespace detail { + template + struct transaction_extension_types { + using transaction_extensions_t = fc::static_variant< Ts... >; + using decompose_t = decompose< Ts... >; + }; + } + + using transaction_extension_types = detail::transaction_extension_types< + deferred_transaction_generation_context + >; + + using transaction_extensions = transaction_extension_types::transaction_extensions_t; + /** * The transaction header contains the fixed-sized data * associated with each transaction. It is separated from @@ -75,6 +108,7 @@ namespace eosio { namespace chain { return account_name(); } + vector validate_and_extract_extensions()const; }; struct signed_transaction : public transaction @@ -174,47 +208,11 @@ namespace eosio { namespace chain { using packed_transaction_ptr = std::shared_ptr; - /** - * When a transaction is generated it can be scheduled to occur - * in the future. It may also fail to execute for some reason in - * which case the sender needs to be notified. When the sender - * sends a transaction they will assign it an ID which will be - * passed back to the sender if the transaction fails for some - * reason. - */ - struct deferred_transaction : public signed_transaction - { - uint128_t sender_id; /// ID assigned by sender of generated, accessible via WASM api when executing normal or error - account_name sender; /// receives error handler callback - account_name payer; - time_point_sec execute_after; /// delayed execution - - deferred_transaction() = default; - - deferred_transaction(uint128_t sender_id, account_name sender, account_name payer,time_point_sec execute_after, - const signed_transaction& txn) - : signed_transaction(txn), - sender_id(sender_id), - sender(sender), - payer(payer), - execute_after(execute_after) - {} - }; - - struct deferred_reference { - deferred_reference(){} - deferred_reference( const account_name& sender, const uint128_t& sender_id) - :sender(sender),sender_id(sender_id) - {} - - account_name sender; - uint128_t sender_id; - }; - uint128_t transaction_id_to_sender_id( const transaction_id_type& tid ); } } /// namespace eosio::chain +FC_REFLECT(eosio::chain::deferred_transaction_generation_context, (sender_trx_id)(sender_id)(sender) ) FC_REFLECT( eosio::chain::transaction_header, (expiration)(ref_block_num)(ref_block_prefix) (max_net_usage_words)(max_cpu_usage_ms)(delay_sec) ) FC_REFLECT_DERIVED( eosio::chain::transaction, (eosio::chain::transaction_header), (context_free_actions)(actions)(transaction_extensions) ) @@ -222,5 +220,3 @@ FC_REFLECT_DERIVED( eosio::chain::signed_transaction, (eosio::chain::transaction FC_REFLECT_ENUM( eosio::chain::packed_transaction::compression_type, (none)(zlib)) // @ignore unpacked_trx FC_REFLECT( eosio::chain::packed_transaction, (signatures)(compression)(packed_context_free_data)(packed_trx) ) -FC_REFLECT_DERIVED( eosio::chain::deferred_transaction, (eosio::chain::signed_transaction), (sender_id)(sender)(payer)(execute_after) ) -FC_REFLECT( eosio::chain::deferred_reference, (sender)(sender_id) ) diff --git a/libraries/chain/include/eosio/chain/transaction_context.hpp b/libraries/chain/include/eosio/chain/transaction_context.hpp index bbfe53810f6..87f31d42727 100644 --- a/libraries/chain/include/eosio/chain/transaction_context.hpp +++ b/libraries/chain/include/eosio/chain/transaction_context.hpp @@ -86,6 +86,8 @@ namespace eosio { namespace chain { void validate_cpu_usage_to_bill( int64_t u, bool check_minimum = true )const; + void disallow_transaction_extensions( const char* error_msg )const; + /// Fields: public: diff --git a/libraries/chain/include/eosio/chain/types.hpp b/libraries/chain/include/eosio/chain/types.hpp index 1cea911d9d9..d681c349844 100644 --- a/libraries/chain/include/eosio/chain/types.hpp +++ b/libraries/chain/include/eosio/chain/types.hpp @@ -269,6 +269,43 @@ namespace eosio { namespace chain { }; // enum_hash needed to support old gcc compiler of Ubuntu 16.04 + namespace detail { + struct extract_match { + bool enforce_unique = false; + }; + + template + struct decompose; + + template<> + struct decompose<> { + template + static auto extract( uint16_t id, const vector& data, ResultVariant& result ) + -> fc::optional + { + return {}; + } + }; + + template + struct decompose { + using head_t = T; + using tail_t = decompose< Rest... >; + + template + static auto extract( uint16_t id, const vector& data, ResultVariant& result ) + -> fc::optional + { + if( id == head_t::extension_id() ) { + result = fc::raw::unpack( data ); + return { extract_match{ head_t::enforce_unique() } }; + } + + return tail_t::template extract( id, data, result ); + } + }; + } + } } // eosio::chain FC_REFLECT( eosio::chain::void_t, ) diff --git a/libraries/chain/protocol_feature_manager.cpp b/libraries/chain/protocol_feature_manager.cpp index a4d60baa273..56cefb770be 100644 --- a/libraries/chain/protocol_feature_manager.cpp +++ b/libraries/chain/protocol_feature_manager.cpp @@ -53,6 +53,19 @@ Also corrects the RAM usage of accounts affected by the replace deferred transac */ {} } ) + ( builtin_protocol_feature_t::no_duplicate_deferred_id, builtin_protocol_feature_spec{ + "NO_DUPLICATE_DEFERRED_ID", + fc::variant("45967387ee92da70171efd9fefd1ca8061b5efe6f124d269cd2468b47f1575a0").as(), + // SHA256 hash of the raw message below within the comment delimiters (do not modify message below). +/* +Builtin protocol feature: NO_DUPLICATE_DEFERRED_ID +Depends on: REPLACE_DEFERRED + +Ensures transactions generated by contracts for deferred execution are adjusted to avoid transaction ID conflicts. +Also allows a contract to send a deferred transaction in a manner that enables the contract to know the transaction ID ahead of time. +*/ + {builtin_protocol_feature_t::replace_deferred} + } ) ( builtin_protocol_feature_t::fix_linkauth_restriction, builtin_protocol_feature_spec{ "FIX_LINKAUTH_RESTRICTION", fc::variant("a98241c83511dc86c857221b9372b4aa7cea3aaebc567a48604e1d3db3557050").as(), diff --git a/libraries/chain/transaction.cpp b/libraries/chain/transaction.cpp index e1910ce02eb..1ebdfeccc01 100644 --- a/libraries/chain/transaction.cpp +++ b/libraries/chain/transaction.cpp @@ -50,6 +50,16 @@ typedef multi_index_container< > > recovery_cache_type; +void deferred_transaction_generation_context::reflector_init() { + static_assert( fc::raw::has_feature_reflector_init_on_unpacked_reflected_types, + "deferred_transaction_generation_context expects FC to support reflector_init" ); + + + EOS_ASSERT( sender != account_name(), ill_formed_deferred_transaction_generation_context, + "Deferred transaction generation context extension must have a non-empty sender account", + ); +} + void transaction_header::set_reference_block( const block_id_type& reference_block ) { ref_block_num = fc::endian_reverse_u32(reference_block._hash[0]); ref_block_prefix = reference_block._hash[1]; @@ -134,6 +144,45 @@ fc::microseconds transaction::get_signature_keys( const vector& return sig_cpu_usage; } FC_CAPTURE_AND_RETHROW() } +vector transaction::validate_and_extract_extensions()const { + using transaction_extensions_t = transaction_extension_types::transaction_extensions_t; + using decompose_t = transaction_extension_types::decompose_t; + + static_assert( std::is_same::value, + "transaction_extensions is not setup as expected" ); + + vector results; + + uint16_t id_type_lower_bound = 0; + + for( size_t i = 0; i < transaction_extensions.size(); ++i ) { + const auto& e = transaction_extensions[i]; + auto id = e.first; + + EOS_ASSERT( id >= id_type_lower_bound, invalid_transaction_extension, + "Transaction extensions are not in the correct order (ascending id types required)" + ); + + results.emplace_back(); + + auto match = decompose_t::extract( id, e.second, results.back() ); + EOS_ASSERT( match, invalid_transaction_extension, + "Transaction extension with id type ${id} is not supported", + ("id", id) + ); + + if( match->enforce_unique ) { + EOS_ASSERT( i == 0 || id > id_type_lower_bound, invalid_transaction_extension, + "Transaction extension with id type ${id} is not allowed to repeat", + ("id", id) + ); + } + + id_type_lower_bound = id; + } + + return results; +} const signature_type& signed_transaction::sign(const private_key_type& key, const chain_id_type& chain_id) { signatures.push_back(key.sign(sig_digest(chain_id, context_free_data))); diff --git a/libraries/chain/transaction_context.cpp b/libraries/chain/transaction_context.cpp index 2f196687d1f..f4222b58a54 100644 --- a/libraries/chain/transaction_context.cpp +++ b/libraries/chain/transaction_context.cpp @@ -166,7 +166,14 @@ namespace bacc = boost::accumulators; trace->block_time = c.pending_block_time(); trace->producer_block_id = c.pending_producer_block_id(); executed.reserve( trx.total_actions() ); - EOS_ASSERT( trx.transaction_extensions.size() == 0, unsupported_feature, "we don't support any extensions yet" ); + } + + void transaction_context::disallow_transaction_extensions( const char* error_msg )const { + if( control.is_producing_block() ) { + EOS_THROW( subjective_block_production_exception, error_msg ); + } else { + EOS_THROW( disallowed_transaction_extensions_bad_block_exception, error_msg ); + } } void transaction_context::init(uint64_t initial_net_usage) @@ -282,6 +289,10 @@ namespace bacc = boost::accumulators; void transaction_context::init_for_implicit_trx( uint64_t initial_net_usage ) { + if( trx.transaction_extensions.size() > 0 ) { + disallow_transaction_extensions( "no transaction extensions supported yet for implicit transactions" ); + } + published = control.pending_block_time(); init( initial_net_usage); } @@ -290,6 +301,10 @@ namespace bacc = boost::accumulators; uint64_t packed_trx_prunable_size, bool skip_recording ) { + if( trx.transaction_extensions.size() > 0 ) { + disallow_transaction_extensions( "no transaction extensions supported yet for input transactions" ); + } + const auto& cfg = control.get_global_properties().configuration; uint64_t discounted_size_for_pruned_data = packed_trx_prunable_size; @@ -326,6 +341,14 @@ namespace bacc = boost::accumulators; void transaction_context::init_for_deferred_trx( fc::time_point p ) { + if( (trx.expiration.sec_since_epoch() != 0) && (trx.transaction_extensions.size() > 0) ) { + disallow_transaction_extensions( "no transaction extensions supported yet for deferred transactions" ); + } + // If (trx.expiration.sec_since_epoch() == 0) then it was created after NO_DUPLICATE_DEFERRED_ID activation, + // and so validation of its extensions was done either in: + // * apply_context::schedule_deferred_transaction for contract-generated transactions; + // * or transaction_context::init_for_input_trx for delayed input transactions. + published = p; trace->scheduled = true; apply_context_free = false; diff --git a/testnet.template b/testnet.template index ab9051f0601..e36e8ba4f80 100644 --- a/testnet.template +++ b/testnet.template @@ -17,8 +17,6 @@ if [ -z "$bioscontractpath" ]; then bioscontractpath="unittests/contracts/eosio.bios" fi -featuredigests=($FEATURE_DIGESTS) - wddir=eosio-ignition-wd wdaddr=localhost:8899 wdurl=http://$wdaddr @@ -42,6 +40,8 @@ mkdir $wddir step=1 echo Initializing ignition sequence at $(date) | tee $logfile +echo "FEATURE_DIGESTS: $FEATURE_DIGESTS" >> $logfile + echo "http-server-address = $wdaddr" > $wddir/config.ini programs/keosd/keosd --config-dir $wddir --data-dir $wddir 2> $wddir/wdlog.txt & @@ -85,7 +85,7 @@ wcmd create --to-console -n ignition ecmd set contract eosio $bioscontractpath eosio.bios.wasm eosio.bios.abi # Preactivate all digests -for digest in "${featuredigests[@]}"; +for digest in $FEATURE_DIGESTS; do ecmd push action eosio preactivate "{\"feature_digest\":\"$digest\"}" -p eosio done diff --git a/tests/Cluster.py b/tests/Cluster.py index 3ccbabe43aa..f38c1ecd3c1 100644 --- a/tests/Cluster.py +++ b/tests/Cluster.py @@ -955,6 +955,7 @@ def bios_bootstrap(self, biosNode, totalNodes, pfSetupPolicy, silent=False): if pfSetupPolicy == PFSetupPolicy.FULL: allBuiltinProtocolFeatureDigests = biosNode.getAllBuiltinFeatureDigestsToPreactivate() env["FEATURE_DIGESTS"] = " ".join(allBuiltinProtocolFeatureDigests) + Utils.Print("Set FEATURE_DIGESTS to: %s" % env["FEATURE_DIGESTS"]) if 0 != subprocess.call(cmd.split(), stdout=Utils.FNull, env=env): if not silent: Utils.Print("Launcher failed to shut down eos cluster.") diff --git a/tests/Node.py b/tests/Node.py index 334d9d2e7d5..795c3d41f0e 100644 --- a/tests/Node.py +++ b/tests/Node.py @@ -1505,15 +1505,19 @@ def activatePreactivateFeature(self): # Wait for the next block to be produced so the scheduled protocol feature is activated self.waitForHeadToAdvance() - # Return an array of feature digests to be preactivated + # Return an array of feature digests to be preactivated in a correct order respecting dependencies # Require producer_api_plugin def getAllBuiltinFeatureDigestsToPreactivate(self): protocolFeatures = [] - protocolFeatureDict = self.getSupportedProtocolFeatureDict() - for k, v in protocolFeatureDict.items(): - # Filter out "PREACTIVATE_FEATURE" - if k != "PREACTIVATE_FEATURE": - protocolFeatures.append(v["feature_digest"]) + supportedProtocolFeatures = self.getSupportedProtocolFeatures() + for protocolFeature in supportedProtocolFeatures: + for spec in protocolFeature["specification"]: + if (spec["name"] == "builtin_feature_codename"): + codename = spec["value"] + # Filter out "PREACTIVATE_FEATURE" + if codename != "PREACTIVATE_FEATURE": + protocolFeatures.append(protocolFeature["feature_digest"]) + break return protocolFeatures # Require PREACTIVATE_FEATURE to be activated and require eosio.bios with preactivate_feature diff --git a/unittests/protocol_feature_tests.cpp b/unittests/protocol_feature_tests.cpp index 3477e83a97c..92a929ead47 100644 --- a/unittests/protocol_feature_tests.cpp +++ b/unittests/protocol_feature_tests.cpp @@ -4,6 +4,7 @@ */ #include #include +#include #include #include @@ -510,6 +511,137 @@ BOOST_AUTO_TEST_CASE( replace_deferred_test ) try { } FC_LOG_AND_RETHROW() +BOOST_AUTO_TEST_CASE( no_duplicate_deferred_id_test ) try { + tester c( setup_policy::preactivate_feature_and_new_bios ); + tester c2( setup_policy::none ); + + c.create_accounts( {N(alice), N(test)} ); + c.set_code( N(test), contracts::deferred_test_wasm() ); + c.set_abi( N(test), contracts::deferred_test_abi().data() ); + c.produce_block(); + + push_blocks( c, c2 ); + + c2.push_action( N(test), N(defercall), N(alice), fc::mutable_variant_object() + ("payer", "alice") + ("sender_id", 1) + ("contract", "test") + ("payload", 50) + ); + + c2.finish_block(); + + BOOST_CHECK_EXCEPTION( + c2.produce_block(), + fc::exception, + fc_exception_message_is( "no transaction extensions supported yet for deferred transactions" ) + ); + + c2.produce_empty_block( fc::minutes(10) ); + + transaction_trace_ptr trace0; + auto h = c2.control->applied_transaction.connect( [&]( const transaction_trace_ptr& t) { + if( t && t->receipt && t->receipt->status == transaction_receipt::expired) { + trace0 = t; + } + } ); + + c2.produce_block(); + + h.disconnect(); + + BOOST_REQUIRE( trace0 ); + + c.produce_block(); + + const auto& pfm = c.control->get_protocol_feature_manager(); + + auto d1 = pfm.get_builtin_digest( builtin_protocol_feature_t::replace_deferred ); + BOOST_REQUIRE( d1 ); + auto d2 = pfm.get_builtin_digest( builtin_protocol_feature_t::no_duplicate_deferred_id ); + BOOST_REQUIRE( d2 ); + + c.preactivate_protocol_features( {*d1, *d2} ); + c.produce_block(); + + auto& index = c.control->db().get_index(); + + auto check_generation_context = []( auto&& data, + const transaction_id_type& sender_trx_id, + unsigned __int128 sender_id, + account_name sender ) + { + transaction trx; + fc::datastream ds1( data.data(), data.size() ); + fc::raw::unpack( ds1, trx ); + BOOST_REQUIRE_EQUAL( trx.transaction_extensions.size(), 1 ); + BOOST_REQUIRE_EQUAL( trx.transaction_extensions.back().first, 0 ); + + fc::datastream ds2( trx.transaction_extensions.back().second.data(), + trx.transaction_extensions.back().second.size() ); + + transaction_id_type actual_sender_trx_id; + fc::raw::unpack( ds2, actual_sender_trx_id ); + BOOST_CHECK_EQUAL( actual_sender_trx_id, sender_trx_id ); + + unsigned __int128 actual_sender_id; + fc::raw::unpack( ds2, actual_sender_id ); + BOOST_CHECK( actual_sender_id == sender_id ); + + uint64_t actual_sender; + fc::raw::unpack( ds2, actual_sender ); + BOOST_CHECK_EQUAL( account_name(actual_sender), sender ); + }; + + BOOST_CHECK_EXCEPTION( + c.push_action( N(test), N(defercall), N(alice), fc::mutable_variant_object() + ("payer", "alice") + ("sender_id", 1) + ("contract", "test") + ("payload", 77 ) + ), + ill_formed_deferred_transaction_generation_context, + fc_exception_message_is( "deferred transaction generaction context contains mismatching sender" ) + ); + + BOOST_REQUIRE_EQUAL(0, index.size()); + + auto trace1 = c.push_action( N(test), N(defercall), N(alice), fc::mutable_variant_object() + ("payer", "alice") + ("sender_id", 1) + ("contract", "test") + ("payload", 40) + ); + + BOOST_REQUIRE_EQUAL(1, index.size()); + + check_generation_context( index.begin()->packed_trx, + trace1->id, + ((static_cast(N(alice)) << 64) | 1), + N(test) ); + + c.produce_block(); + + BOOST_REQUIRE_EQUAL(0, index.size()); + + auto trace2 = c.push_action( N(test), N(defercall), N(alice), fc::mutable_variant_object() + ("payer", "alice") + ("sender_id", 1) + ("contract", "test") + ("payload", 50) + ); + + BOOST_REQUIRE_EQUAL(1, index.size()); + + check_generation_context( index.begin()->packed_trx, + trace2->id, + ((static_cast(N(alice)) << 64) | 1), + N(test) ); + + c.produce_block(); + +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_CASE( fix_linkauth_restriction ) { try { tester chain( setup_policy::preactivate_feature_and_new_bios ); diff --git a/unittests/test-contracts/README.md b/unittests/test-contracts/README.md index 157455c7202..aa9c0f8dee9 100644 --- a/unittests/test-contracts/README.md +++ b/unittests/test-contracts/README.md @@ -2,6 +2,6 @@ test_ram_limit contract was compiled with eosio.cdt v1.4.1 That contract was ported to compile with eosio.cdt v1.5.0, but the test that uses it is very sensitive to stdlib/eosiolib changes, compilation flags and linker flags. -deferred_test contract was compiled with eosio.cdt v1.6.1 +deferred_test and proxy contracts were compiled with eosio.cdt v1.6.1 The remaining contracts have been ported to compile with eosio.cdt v1.6.x. They were compiled with a patched version of eosio.cdt v1.6.0-rc1 (commit 1c9180ff5a1e431385180ce459e11e6a1255c1a4). diff --git a/unittests/test-contracts/deferred_test/deferred_test.cpp b/unittests/test-contracts/deferred_test/deferred_test.cpp index 4ee7465537c..b24096de146 100644 --- a/unittests/test-contracts/deferred_test/deferred_test.cpp +++ b/unittests/test-contracts/deferred_test/deferred_test.cpp @@ -4,6 +4,8 @@ */ #include "deferred_test.hpp" #include +#include +#include using namespace eosio; @@ -16,6 +18,25 @@ void deferred_test::defercall( name payer, uint64_t sender_id, name contract, ui deferfunc_action a( contract, {get_self(), "active"_n} ); trx.actions.emplace_back( a.to_action( payload ) ); bool replace_existing = (payload >= 100); + + if( (50 <= payload && payload < 100) || payload >= 150 ) { + size_t tx_size = transaction_size(); + char* buffer = (char*)malloc( tx_size ); + read_transaction( buffer, tx_size ); + auto tx_id = sha256( buffer, tx_size ); + char context_buffer[56]; + trx.transaction_extensions.emplace_back( (uint16_t)0, std::vector() ); + auto& context_vector = std::get<1>( trx.transaction_extensions.back() ); + context_vector.resize(56); + datastream ds( context_vector.data(), 56 ); + ds << tx_id.extract_as_byte_array(); + ds << ((static_cast(payer.value) << 64) | sender_id); + if( payload != 77 ) + ds << get_self(); + else + ds << payer; + } + trx.send( (static_cast(payer.value) << 64) | sender_id, payer, replace_existing ); } diff --git a/unittests/test-contracts/deferred_test/deferred_test.wasm b/unittests/test-contracts/deferred_test/deferred_test.wasm index fbfdaf14f08..588e38fadf2 100755 Binary files a/unittests/test-contracts/deferred_test/deferred_test.wasm and b/unittests/test-contracts/deferred_test/deferred_test.wasm differ diff --git a/unittests/test-contracts/proxy/proxy.cpp b/unittests/test-contracts/proxy/proxy.cpp index 1a199c4a5ba..c9fc324cad5 100644 --- a/unittests/test-contracts/proxy/proxy.cpp +++ b/unittests/test-contracts/proxy/proxy.cpp @@ -59,6 +59,7 @@ void proxy::on_error( uint128_t sender_id, eosio::ignore> ) { get_datastream() >> packed_trx_size; transaction trx; get_datastream() >> trx; + trx.transaction_extensions.clear(); trx.delay_sec = cfg.delay; trx.send( id, get_self() ); diff --git a/unittests/test-contracts/proxy/proxy.wasm b/unittests/test-contracts/proxy/proxy.wasm index c09311385be..b40249782ba 100755 Binary files a/unittests/test-contracts/proxy/proxy.wasm and b/unittests/test-contracts/proxy/proxy.wasm differ