diff --git a/libraries/chain/apply_context.cpp b/libraries/chain/apply_context.cpp index 9b5930b805383af5c5f91191decfcc4ea42e0eab..9bf18a090735075cfeec3e1f2fe5bb9b36857c58 100644 --- a/libraries/chain/apply_context.cpp +++ b/libraries/chain/apply_context.cpp @@ -137,7 +137,10 @@ void apply_context::require_recipient( account_name code ) { } void apply_context::execute_inline( action &&a ) { - controller.check_authorization( {a}, flat_set(), false, {receiver} ); + // todo: rethink this special case + if (receiver != config::system_account_name) { + controller.check_authorization({a}, flat_set(), false, {receiver}); + } _inline_actions.emplace_back( move(a) ); } @@ -153,7 +156,10 @@ void apply_context::execute_deferred( deferred_transaction&& trx ) { FC_ASSERT( !trx.actions.empty(), "transaction must have at least one action"); - controller.check_authorization( trx.actions, flat_set(), false, {receiver} ); + // todo: rethink this special case + if (receiver != config::system_account_name) { + controller.check_authorization(trx.actions, flat_set(), false, {receiver}); + } controller.validate_scope( trx ); diff --git a/libraries/chain/chain_controller.cpp b/libraries/chain/chain_controller.cpp index 4a8707d9a3d0a51d901c1c70113e6700b815ac33..d4a4774af65ff99c24bc962afe63990c053a9b1d 100644 --- a/libraries/chain/chain_controller.cpp +++ b/libraries/chain/chain_controller.cpp @@ -675,15 +675,19 @@ void chain_controller::check_authorization( const vector& actions, for( const auto& act : actions ) { for( const auto& declared_auth : act.authorization ) { - const auto& min_permission = lookup_minimum_permission(declared_auth.actor, - act.scope, act.name); - - if ((_skip_flags & skip_authority_check) == false) { - const auto& index = _db.get_index().indices(); - EOS_ASSERT(get_permission(declared_auth).satisfies(min_permission, index), - tx_irrelevant_auth, - "action declares irrelevant authority '${auth}'; minimum authority is ${min}", - ("auth", declared_auth)("min", min_permission.name)); + // check a minimum permission if one is set, otherwise assume the contract code will validate + auto min_permission_name = lookup_minimum_permission(declared_auth.actor, act.scope, act.name); + if (min_permission_name) { + const auto& min_permission = _db.get(boost::make_tuple(declared_auth.actor, *min_permission_name)); + + + if ((_skip_flags & skip_authority_check) == false) { + const auto &index = _db.get_index().indices(); + EOS_ASSERT(get_permission(declared_auth).satisfies(min_permission, index), + tx_irrelevant_auth, + "action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", declared_auth)("min", min_permission.name)); + } } if ((_skip_flags & skip_transaction_signatures) == false) { EOS_ASSERT(checker.satisfied(declared_auth), tx_missing_sigs, @@ -737,9 +741,15 @@ void chain_controller::validate_scope( const transaction& trx )const { FC_ASSERT( intersection.size() == 0, "a transaction may not redeclare scope in readscope" ); } -const permission_object& chain_controller::lookup_minimum_permission(account_name authorizer_account, +optional chain_controller::lookup_minimum_permission(account_name authorizer_account, account_name scope, action_name act_name) const { + // updateauth is a special case where any permission _may_ be suitable depending + // on the contents of the action + if (scope == config::system_account_name && act_name == N(updateauth)) { + return optional(); + } + try { // First look up a specific link for this message act_name auto key = boost::make_tuple(authorizer_account, scope, act_name); @@ -751,10 +761,10 @@ const permission_object& chain_controller::lookup_minimum_permission(account_nam } // If no specific or default link found, use active permission - auto permission_key = boost::make_tuple(authorizer_account, config::active_name ); if (link != nullptr) - get<1>(permission_key) = link->required_permission; - return _db.get(permission_key); + return link->required_permission; + else + return N(active); } FC_CAPTURE_AND_RETHROW((authorizer_account)(scope)(act_name)) } @@ -1141,18 +1151,20 @@ void chain_controller::update_global_dynamic_data(const signed_block& b) { // if (missed_blocks) // wlog("Blockchain continuing after gap of ${b} missed blocks", ("b", missed_blocks)); - for(uint32_t i = 0; i < missed_blocks; ++i) { - const auto& producer_missed = get_producer(get_scheduled_producer(i+1)); - if(producer_missed.owner != b.producer) { - /* - const auto& producer_account = producer_missed.producer_account(*this); - if( (fc::time_point::now() - b.timestamp) < fc::seconds(30) ) - wlog( "Producer ${name} missed block ${n} around ${t}", ("name",producer_account.name)("n",b.block_num())("t",b.timestamp) ); - */ - - _db.modify( producer_missed, [&]( producer_object& w ) { - w.total_missed++; - }); + if (!(_skip_flags & skip_missed_block_penalty)) { + for (uint32_t i = 0; i < missed_blocks; ++i) { + const auto &producer_missed = get_producer(get_scheduled_producer(i + 1)); + if (producer_missed.owner != b.producer) { + /* + const auto& producer_account = producer_missed.producer_account(*this); + if( (fc::time_point::now() - b.timestamp) < fc::seconds(30) ) + wlog( "Producer ${name} missed block ${n} around ${t}", ("name",producer_account.name)("n",b.block_num())("t",b.timestamp) ); + */ + + _db.modify(producer_missed, [&](producer_object &w) { + w.total_missed++; + }); + } } } @@ -1541,10 +1553,18 @@ void chain_controller::update_usage( transaction_metadata& meta, uint32_t act_us // for any transaction not sent by code, update the affirmative last time a given permission was used if (!meta.sender) { - const auto &puo = _db.get(boost::make_tuple(authaccnt.first, authaccnt.second)); - _db.modify(puo, [this](auto &pu) { - pu.last_used = head_block_time(); - }); + const auto *puo = _db.find(boost::make_tuple(authaccnt.first, authaccnt.second)); + if (puo) { + _db.modify(*puo, [this](permission_usage_object &pu) { + pu.last_used = head_block_time(); + }); + } else { + _db.create([this, &authaccnt](permission_usage_object &pu){ + pu.account = authaccnt.first; + pu.permission = authaccnt.second; + pu.last_used = head_block_time(); + }); + } } } diff --git a/libraries/chain/contracts/eosio_contract.cpp b/libraries/chain/contracts/eosio_contract.cpp index d025ceebe18ec75b859e32865f316eb97a351d51..5d2c520d1ac4bd5053735278ac4d8ce34ab614be 100644 --- a/libraries/chain/contracts/eosio_contract.cpp +++ b/libraries/chain/contracts/eosio_contract.cpp @@ -107,12 +107,6 @@ void apply_eosio_newaccount(apply_context& context) { }); auto create_permission = [owner=create.name, &db, &context](const permission_name& name, permission_object::id_type parent, authority &&auth) { - db.create([&](permission_usage_object& pu){ - pu.account = owner; - pu.permission = name; - pu.last_used = context.controller.head_block_time(); - }); - return db.create([&](permission_object& p) { p.name = name; p.parent = parent; @@ -431,10 +425,27 @@ void apply_eosio_updateauth(apply_context& context) { auto& db = context.mutable_db; - bool is_system = context.act.authorization.size() == 1 && context.act.authorization.front().actor == config::system_account_name && context.sender && context.sender == config::system_account_name; - if (!is_system) { - context.require_authorization(update.account); - } + FC_ASSERT(context.act.authorization.size(), "updateauth can only have one action authorization"); + const auto& act_auth = context.act.authorization.front(); + // lazy evaluating loop + auto permission_is_valid_for_update = [&](){ + if (act_auth.permission == config::owner_name || act_auth.permission == update.permission) { + return true; + } + + auto current = db.get(boost::make_tuple(update.account, update.permission)); + while(current.name != config::owner_name) { + if (current.name == act_auth.permission) { + return true; + } + + current = db.get(current.parent); + } + + return false; + }; + + FC_ASSERT(act_auth.actor == update.account && permission_is_valid_for_update(), "updateauth must carry a permission equal to or in the ancestery of permission it updates"); db.get(update.account); validate_authority_precondition(context, update.data); @@ -561,26 +572,29 @@ static const abi_serializer& get_abi_serializer() { } static optional get_pending_recovery(apply_context& context, account_name account ) { - const auto* t_id = context.find_table(account, config::system_account_name, N(pending_recovery)); + ilog("Fetching recovery for ${account}:", ("account", account)); + const auto* t_id = context.find_table(account, config::system_account_name, N(recovery)); if (t_id) { - uint64_t key = N(account); + uint64_t key = account; int32_t record_size = context.front_record(*t_id, &key, nullptr, 0); if (record_size > 0) { - bytes value(record_size); - record_size = context.front_record(*t_id, &key, value.data(), value.size()); - assert(record_size == value.size()); + bytes value(record_size + sizeof(uint64_t)); + uint64_t* key_p = reinterpret_cast(value.data()); + *key_p = key; + + record_size = context.front_record(*t_id, &key, value.data() + sizeof(uint64_t), value.size() - sizeof(uint64_t)); + assert(record_size == value.size() - sizeof(uint64_t)); return get_abi_serializer().binary_to_variant("pending_recovery", value); } } - return optional(); } static uint32_t get_next_sender_id(apply_context& context) { context.require_write_scope( config::eosio_auth_scope ); - const auto& t_id = context.find_or_create_table(config::eosio_auth_scope, config::system_account_name, N(deferred_sequence)); + const auto& t_id = context.find_or_create_table(config::eosio_auth_scope, config::system_account_name, N(deferred.seq)); uint64_t key = N(config::eosio_auth_scope); uint32_t next_serial = 0; context.front_record(t_id, &key, (char *)&next_serial, sizeof(uint32_t)); @@ -590,6 +604,20 @@ static uint32_t get_next_sender_id(apply_context& context) { return result; } +static auto get_account_creation(const apply_context& context, const account_name& account) { + auto const& accnt = context.db.get(account); + return (time_point)accnt.creation_date; +}; + +static auto get_permission_last_used(const apply_context& context, const account_name& account, const permission_name& permission) { + auto const* perm = context.db.find(boost::make_tuple(account, permission)); + if (perm) { + return optional(perm->last_used); + } + + return optional(); +}; + void apply_eosio_postrecovery(apply_context& context) { context.require_write_scope( config::eosio_auth_scope ); @@ -609,11 +637,23 @@ void apply_eosio_postrecovery(apply_context& context) { delay_lock = fc::days(30); } else { // process lost password - auto const& owner_perm = context.db.get(boost::make_tuple(account, N(owner))); - auto const& active_perm = context.db.get(boost::make_tuple(account, N(active))); - FC_ASSERT(owner_perm.last_used >= now - fc::days(30), "Account ${account} has had owner key activity recently and cannot be recovered yet!", ("account",account)); - FC_ASSERT(active_perm.last_used >= now - fc::days(30), "Account ${account} has had active key activity recently and cannot be recovered yet!", ("account",account)); + auto owner_last_used = get_permission_last_used(context, account, N(owner)); + auto active_last_used = get_permission_last_used(context, account, N(active)); + + if (!owner_last_used || !active_last_used) { + auto account_creation = get_account_creation(context, account); + if (!owner_last_used) { + owner_last_used.emplace(account_creation); + } + + if (!active_last_used) { + active_last_used.emplace(account_creation); + } + } + + FC_ASSERT(*owner_last_used <= now - fc::days(30), "Account ${account} has had owner key activity recently and cannot be recovered yet!", ("account",account)); + FC_ASSERT(*active_last_used <= now - fc::days(30), "Account ${account} has had active key activity recently and cannot be recovered yet!", ("account",account)); delay_lock = fc::days(7); } @@ -628,8 +668,8 @@ void apply_eosio_postrecovery(apply_context& context) { uint32_t request_id = get_next_sender_id(context); - auto record = mutable_variant_object() - ("account", (string)account) + auto record_data = mutable_variant_object() + ("account", account) ("request_id", request_id) ("update", update) ("memo", recover_act.memo); @@ -641,29 +681,33 @@ void apply_eosio_postrecovery(apply_context& context) { dtrx.execute_after = context.controller.head_block_time() + delay_lock; dtrx.set_reference_block(context.controller.head_block_id()); dtrx.expiration = dtrx.execute_after + fc::seconds(60); - dtrx.write_scope = sort_names({account, config::eosio_auth_scope}); - dtrx.actions.emplace_back(vector{{config::system_account_name,config::active_name}}, + dtrx.write_scope = sort_names({account, config::eosio_auth_scope, config::system_account_name}); + dtrx.actions.emplace_back(vector{{account,config::active_name}}, passrecovery { account }); context.execute_deferred(std::move(dtrx)); - const auto& t_id = context.find_or_create_table(account, config::system_account_name, N(pending_recovery)); - auto data = get_abi_serializer().variant_to_binary("pending_recovery", record); - context.store_record(t_id,(uint64_t *)data.data(), data.data() + sizeof(uint64_t), data.size() - sizeof(uint64_t)); + + const auto& t_id = context.find_or_create_table(account, config::system_account_name, N(recovery)); + auto data = get_abi_serializer().variant_to_binary("pending_recovery", record_data); + context.store_record(t_id,&account.value, data.data() + sizeof(uint64_t), data.size() - sizeof(uint64_t)); context.console_append_formatted("Recovery Started for account ${account} : ${memo}\n", mutable_variant_object()("account", account)("memo", recover_act.memo)); } static void remove_pending_recovery(apply_context& context, const account_name& account) { - const auto& t_id = context.find_or_create_table(account, config::system_account_name, N(pending_recovery)); + const auto& t_id = context.find_or_create_table(account, config::system_account_name, N(recovery)); context.remove_record(t_id, &account.value); } void apply_eosio_passrecovery(apply_context& context) { - context.require_authorization(config::system_account_name); auto pass_act = context.act.as(); const auto& account = pass_act.account; + // ensure this is only processed if it is a deferred transaction from the system account + FC_ASSERT(context.sender && *context.sender == config::system_account_name); + context.require_authorization(account); + auto maybe_recovery = get_pending_recovery(context, account); FC_ASSERT(maybe_recovery, "No pending recovery found for account ${account}", ("account", account)); auto recovery = *maybe_recovery; @@ -671,7 +715,7 @@ void apply_eosio_passrecovery(apply_context& context) { updateauth update; fc::from_variant(recovery["update"], update); - action act(vector{{config::system_account_name,config::active_name}}, update); + action act(vector{{account,config::owner_name}}, update); context.execute_inline(move(act)); remove_pending_recovery(context, account); diff --git a/libraries/chain/include/eosio/chain/apply_context.hpp b/libraries/chain/include/eosio/chain/apply_context.hpp index 9ea8c306f56edec479dba9b032a5613b203eb23a..ada48aa661d19b4e699796aa4b2b4c9c8a2d406e 100644 --- a/libraries/chain/include/eosio/chain/apply_context.hpp +++ b/libraries/chain/include/eosio/chain/apply_context.hpp @@ -457,11 +457,15 @@ using apply_handler = std::function; impl::key_helper::get(keys, *itr); - auto copylen = std::min(itr->value.size(),valuelen); - if( copylen ) { - itr->value.copy(value, copylen); + if (valuelen) { + auto copylen = std::min(itr->value.size(), valuelen); + if (copylen) { + itr->value.copy(value, copylen); + } + return copylen; + } else { + return itr->value.size(); } - return copylen; } template @@ -477,11 +481,15 @@ using apply_handler = std::function; impl::key_helper::get(keys, *itr); - auto copylen = std::min(itr->value.size(),valuelen); - if( copylen ) { - itr->value.copy(value, copylen); + if (valuelen) { + auto copylen = std::min(itr->value.size(), valuelen); + if (copylen) { + itr->value.copy(value, copylen); + } + return copylen; + } else { + return itr->value.size(); } - return copylen; } template @@ -501,11 +509,15 @@ using apply_handler = std::function; impl::key_helper::get(keys, *itr); - auto copylen = std::min(itr->value.size(),valuelen); - if( copylen ) { - itr->value.copy(value, copylen); + if (valuelen) { + auto copylen = std::min(itr->value.size(), valuelen); + if (copylen) { + itr->value.copy(value, copylen); + } + return copylen; + } else { + return itr->value.size(); } - return copylen; } template @@ -539,12 +551,16 @@ using apply_handler = std::function; } impl::key_helper::get(keys, *itr); - - auto copylen = std::min(itr->value.size(),valuelen); - if( copylen ) { - itr->value.copy(value, copylen); + + if (valuelen) { + auto copylen = std::min(itr->value.size(), valuelen); + if (copylen) { + itr->value.copy(value, copylen); + } + return copylen; + } else { + return itr->value.size(); } - return copylen; } template @@ -575,11 +591,15 @@ using apply_handler = std::function; impl::key_helper::get(keys, *itr); - auto copylen = std::min(itr->value.size(),valuelen); - if( copylen ) { - itr->value.copy(value, copylen); + if (valuelen) { + auto copylen = std::min(itr->value.size(), valuelen); + if (copylen) { + itr->value.copy(value, copylen); + } + return copylen; + } else { + return itr->value.size(); } - return copylen; } template @@ -595,11 +615,15 @@ using apply_handler = std::function; impl::key_helper::get(keys, *itr); - auto copylen = std::min(itr->value.size(),valuelen); - if( copylen ) { - itr->value.copy(value, copylen); + if (valuelen) { + auto copylen = std::min(itr->value.size(), valuelen); + if (copylen) { + itr->value.copy(value, copylen); + } + return copylen; + } else { + return itr->value.size(); } - return copylen; } template @@ -615,11 +639,15 @@ using apply_handler = std::function; impl::key_helper::get(keys, *itr); - auto copylen = std::min(itr->value.size(),valuelen); - if( copylen ) { - itr->value.copy(value, copylen); + if (valuelen) { + auto copylen = std::min(itr->value.size(), valuelen); + if (copylen) { + itr->value.copy(value, copylen); + } + return copylen; + } else { + return itr->value.size(); } - return copylen; } } } // namespace eosio::chain diff --git a/libraries/chain/include/eosio/chain/chain_controller.hpp b/libraries/chain/include/eosio/chain/chain_controller.hpp index 0a386a2ad9adafc199d71f1c130d6e91dd0a2269..a108544188c57e16869f626bc0858fdf9559a4ec 100644 --- a/libraries/chain/include/eosio/chain/chain_controller.hpp +++ b/libraries/chain/include/eosio/chain/chain_controller.hpp @@ -52,7 +52,8 @@ namespace eosio { namespace chain { pushed_transaction = 1 << 14, ///< used to indicate that the origination of the call was from a push_transaction, to determine time allotment created_block = 1 << 15, ///< used to indicate that the origination of the call was for creating a block, to determine time allotment received_block = 1 << 16, ///< used to indicate that the origination of the call was for a received block, to determine time allotment - genesis_setup = 1 << 17 ///< used to indicate that the origination of the call was for a genesis transaction + genesis_setup = 1 << 17, ///< used to indicate that the origination of the call was for a genesis transaction + skip_missed_block_penalty = 1 << 18, ///< used to indicate that missed blocks shouldn't count against producers (used in long unit tests) }; @@ -377,7 +378,7 @@ namespace eosio { namespace chain { * @param type The type of message * @return */ - const permission_object& lookup_minimum_permission( account_name authorizer_account, + optional lookup_minimum_permission( account_name authorizer_account, scope_name code_account, action_name type) const; diff --git a/libraries/chain/wasm_interface.cpp b/libraries/chain/wasm_interface.cpp index d7ce47dfcc411eeb9f556f970272bc32a8cd8dd6..122fa6100a95f419ff34e1a94af189955fa653b0 100644 --- a/libraries/chain/wasm_interface.cpp +++ b/libraries/chain/wasm_interface.cpp @@ -253,7 +253,7 @@ namespace eosio { namespace chain { // grab the lock and put this in the cache as unavailble with_lock([&,this]() { // find or create a new entry - auto iter = _cache.emplace(code_id, std::move(code_info(mem_end, std::move(mem_image)))).first; + auto iter = _cache.emplace(code_id, code_info(mem_end, std::move(mem_image))).first; iter->second.instances.emplace_back(std::make_unique(instance, module)); pending_result = optional_entry_ref(*iter->second.instances.back().get()); diff --git a/libraries/testing/tester.cpp b/libraries/testing/tester.cpp index f9d9f4e9e3292dbde9ceb0cbce7dad3932c696b5..4e6f17d07911a118ed669a56ef6623722baf0bde 100644 --- a/libraries/testing/tester.cpp +++ b/libraries/testing/tester.cpp @@ -76,7 +76,7 @@ namespace eosio { namespace testing { auto sch_pro = control->get_scheduled_producer(slot); auto priv_key = get_private_key( sch_pro, "producer" ); - return control->generate_block( next_time, sch_pro, priv_key ); + return control->generate_block( next_time, sch_pro, priv_key, skip_missed_block_penalty ); } @@ -96,7 +96,7 @@ namespace eosio { namespace testing { trx.write_scope = { creator, config::eosio_auth_scope }; trx.actions.emplace_back( vector{{creator,config::active_name}}, - contracts::newaccount{ + contracts::newaccount{ .creator = creator, .name = a, .owner = authority( get_public_key( a, "owner" ) ), diff --git a/tests/chain_tests/recovery_tests.cpp b/tests/chain_tests/recovery_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2bc16c2785d68aa9e265fa8010c528a512d9a54e --- /dev/null +++ b/tests/chain_tests/recovery_tests.cpp @@ -0,0 +1,74 @@ +#include +#include + +#include + + +using namespace eosio; +using namespace eosio::chain; +using namespace eosio::chain::contracts; +using namespace eosio::testing; + +BOOST_AUTO_TEST_SUITE(recovery_tests) + + + +BOOST_FIXTURE_TEST_CASE( test_recovery_owner, tester ) try { + produce_blocks(1000); + create_account(N(alice), asset::from_string("1000.000 EOS")); + produce_block(); + + fc::time_point expected_recovery(fc::seconds(control->head_block_time().sec_since_epoch()) +fc::days(30)); + + transaction_id_type recovery_txid; + { + signed_transaction trx; + trx.write_scope = {config::eosio_auth_scope, N(alice)}; + trx.actions.emplace_back( vector{{N(alice),config::active_name}}, + postrecovery{ + .account = N(alice), + .data = authority(get_public_key(N(alice), "owner.recov")), + .memo = "Test recovery" + } ); + set_tapos(trx); + trx.sign(get_private_key(N(alice), "active"), chain_id_type()); + auto trace = push_transaction(trx); + BOOST_REQUIRE_EQUAL(trace.deferred_transactions.size(), 1); + recovery_txid = trace.deferred_transactions.front().id(); + produce_block(); + BOOST_REQUIRE_EQUAL(chain_has_transaction(trx.id()), true); + } + + auto push_nonce = [this](const string& role) { + // ensure the old owner key is valid + signed_transaction trx; + trx.write_scope = {N(alice)}; + trx.actions.emplace_back( vector{{N(alice),config::owner_name}}, + nonce{ + .value = control->head_block_num() + } ); + set_tapos(trx); + trx.sign(get_private_key(N(alice), role), chain_id_type()); + push_transaction(trx); + return trx.id(); + }; + + auto skip_time = expected_recovery - control->head_block_time() - fc::milliseconds(config::block_interval_ms); + produce_block(skip_time); + control->push_deferred_transactions(true); + auto last_old_nonce_id = push_nonce("owner"); + produce_block(); + control->push_deferred_transactions(true); + + BOOST_REQUIRE_EQUAL(chain_has_transaction(last_old_nonce_id), true); + BOOST_REQUIRE_THROW(push_nonce("owner"), tx_missing_sigs); + auto first_new_nonce_id = push_nonce("owner.recov"); + produce_block(); + BOOST_REQUIRE_EQUAL(chain_has_transaction(first_new_nonce_id), true); + +} FC_LOG_AND_RETHROW() + + + + +BOOST_AUTO_TEST_SUITE_END()