diff --git a/contracts/eosio.system/native.hpp b/contracts/eosio.system/native.hpp index 28cf5e48647a899e853ff2d491ca1a1b78be1e8c..43e25c5b246ab5c5b72bea6cbdfb8b32cdbe4e9f 100644 --- a/contracts/eosio.system/native.hpp +++ b/contracts/eosio.system/native.hpp @@ -132,9 +132,10 @@ namespace eosiosystem { } ACTION( SystemAccount, canceldelay ) { + permission_level canceling_auth; transaction_id_type trx_id; - EOSLIB_SERIALIZE( canceldelay, (trx_id) ) + EOSLIB_SERIALIZE( canceldelay, (canceling_auth)(trx_id) ) }; static void on( const canceldelay& ) { diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 26e2d551d8e459ba6437aac0b2b2ccf14276b6ee..e71d0c9c8b84d0e0e0d9c494341eba92cac1774e 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -10,6 +10,7 @@ add_library( eosio_chain block_state.cpp fork_database.cpp controller.cpp + authorization_manager.cpp resource_limits.cpp block_log.cpp genesis_state.cpp @@ -41,7 +42,7 @@ add_library( eosio_chain ) target_link_libraries( eosio_chain eos_utilities fc chainbase Logging IR WAST WASM Runtime - wasm asmjs passes cfg ast emscripten-optimizer support softfloat + wasm asmjs passes cfg ast emscripten-optimizer support softfloat ) target_include_directories( eosio_chain PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_BINARY_DIR}/include" @@ -52,5 +53,3 @@ target_include_directories( eosio_chain #if(MSVC) # set_source_files_properties( db_init.cpp db_block.cpp database.cpp block_log.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) #endif(MSVC) - - diff --git a/libraries/chain/abi_serializer.cpp b/libraries/chain/abi_serializer.cpp index deed132804c23f08bae0bc70dabf4ecc25ab66ed..0a8704985a1903de68cfe73b82cb251479692238 100644 --- a/libraries/chain/abi_serializer.cpp +++ b/libraries/chain/abi_serializer.cpp @@ -15,7 +15,7 @@ using namespace boost; -namespace eosio { namespace chain { +namespace eosio { namespace chain { using boost::algorithm::ends_with; using std::string; @@ -96,6 +96,7 @@ namespace eosio { namespace chain { built_in_types.emplace("permission_name", pack_unpack()); built_in_types.emplace("action_name", pack_unpack()); built_in_types.emplace("scope_name", pack_unpack()); + built_in_types.emplace("permission_level", pack_unpack()); built_in_types.emplace("producer_schedule", pack_unpack()); built_in_types.emplace("newaccount", pack_unpack()); } diff --git a/libraries/chain/apply_context.cpp b/libraries/chain/apply_context.cpp index 1044c9d3a7ba5511c50db66ec4621fbdc481cf47..538bb71b928b0af8f63d2fe67533ab01c77fb2ad 100644 --- a/libraries/chain/apply_context.cpp +++ b/libraries/chain/apply_context.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -18,7 +19,7 @@ action_trace apply_context::exec_one() const auto& gpo = control.get_global_properties(); auto start = fc::time_point::now(); - cpu_usage = gpo.configuration.base_per_action_cpu_usage; + cpu_usage = gpo.configuration.base_per_action_cpu_usage; try { const auto &a = control.get_account(receiver); privileged = a.privileged; @@ -217,7 +218,7 @@ void apply_context::require_recipient( account_name recipient ) { void apply_context::execute_inline( action&& a ) { if ( !privileged ) { if( a.account != receiver ) { - const auto delay = control.check_authorization({a}, flat_set(), false, {receiver}); + const auto delay = control.get_authorization_manager().check_authorization({a}, flat_set(), false, {receiver}); FC_ASSERT( published_time + delay <= control.head_block_time(), "inline action uses a permission that imposes a delay that is not met, set delay_sec in transaction header to at least ${delay} seconds", ("delay", delay.to_seconds()) ); @@ -239,8 +240,8 @@ void apply_context::schedule_deferred_transaction( deferred_transaction&& trx ) transaction_exception, "Transaction expires at ${trx.expiration} which is before the first allowed time to execute at ${trx.execute_after}", ("trx.expiration",trx.expiration)("trx.execute_after",trx.execute_after) ); - control.validate_referenced_accounts( trx ); - control.validate_expiration( trx ); + control.validate_referenced_accounts( trx ); + control.validate_expiration( trx ); if( !privileged ) { if (trx.payer != receiver) { @@ -265,7 +266,7 @@ void apply_context::schedule_deferred_transaction( deferred_transaction&& trx ) fc::datastream ds( gtx.packed_trx.data(), trx_size ); fc::raw::pack( ds, trx ); }); - + auto& rl = control.get_mutable_resource_limits_manager(); rl.add_pending_account_ram_usage( trx.payer, config::billable_size_v + trx_size ); checktime( trx_size * 4 ); /// 4 instructions per byte of packed generated trx (estimated) @@ -301,7 +302,7 @@ void apply_context::schedule_deferred_transaction( deferred_transaction&& trx ) } } if( check_auth ) { - delay = controller.check_authorization(trx.actions, flat_set(), false, {receiver}); + delay = controller..get_authorization_manager().check_authorization(trx.actions, flat_set(), false, {receiver}); FC_ASSERT( trx_meta.published + delay <= controller.head_block_time(), "deferred transaction uses a permission that imposes a delay that is not met, set delay_sec in transaction header to at least ${delay} seconds", ("delay", delay.to_seconds()) ); @@ -432,11 +433,11 @@ int apply_context::get_context_free_data( uint32_t index, char* buffer, size_t b } void apply_context::check_auth( const transaction& trx, const vector& perm ) { - control.check_authorization( trx.actions, - {}, - true, - {}, - flat_set(perm.begin(), perm.end()) ); + control.get_authorization_manager().check_authorization( trx.actions, + {}, + true, + {}, + flat_set(perm.begin(), perm.end()) ); } int apply_context::db_store_i64( uint64_t scope, uint64_t table, const account_name& payer, uint64_t id, const char* buffer, size_t buffer_size ) { diff --git a/libraries/chain/authorization_manager.cpp b/libraries/chain/authorization_manager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6afbbb05dc84d8ee32e806a84d26f9838bc3d760 --- /dev/null +++ b/libraries/chain/authorization_manager.cpp @@ -0,0 +1,454 @@ +/** + * @file + * @copyright defined in eos/LICENSE.txt + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eosio { namespace chain { + + authorization_manager::authorization_manager(controller& c):_control(c),_db(c.db()){} + + void authorization_manager::add_indices() { + _db.add_index(); + _db.add_index(); + _db.add_index(); + } + + void authorization_manager::initialize_database() { + _db.create([](auto&){}); /// reserve perm 0 (used else where) + } + + const permission_object& authorization_manager::create_permission( account_name account, + permission_name name, + permission_id_type parent, + const authority& auth, + time_point initial_creation_time + ) + { + const auto& perm = _db.create([&](auto& p) { + p.name = name; + p.parent = parent; + p.owner = account; + p.auth = auth; + p.delay = fc::seconds(auth.delay_sec); + if( initial_creation_time == time_point()) + p.last_updated = _control.head_block_time(); + else + p.last_updated = initial_creation_time; + }); + return perm; + } + + const permission_object& authorization_manager::create_permission( account_name account, + permission_name name, + permission_id_type parent, + authority&& auth, + time_point initial_creation_time + ) + { + const auto& perm = _db.create([&](auto& p) { + p.name = name; + p.parent = parent; + p.owner = account; + p.auth = std::move(auth); + p.delay = fc::seconds(auth.delay_sec); + if( initial_creation_time == time_point()) + p.last_updated = _control.head_block_time(); + else + p.last_updated = initial_creation_time; + }); + return perm; + } + + const permission_object* authorization_manager::find_permission( const permission_level& level )const + { try { + FC_ASSERT( !level.actor.empty() && !level.permission.empty(), "Invalid permission" ); + return _db.find( boost::make_tuple(level.actor,level.permission) ); + } EOS_RETHROW_EXCEPTIONS( chain::permission_query_exception, "Failed to retrieve permission: ${level}", ("level", level) ) } + + const permission_object& authorization_manager::get_permission( const permission_level& level )const + { try { + FC_ASSERT( !level.actor.empty() && !level.permission.empty(), "Invalid permission" ); + return _db.get( boost::make_tuple(level.actor,level.permission) ); + } EOS_RETHROW_EXCEPTIONS( chain::permission_query_exception, "Failed to retrieve permission: ${level}", ("level", level) ) } + + optional authorization_manager::lookup_linked_permission( account_name authorizer_account, + account_name scope, + action_name act_name + )const + { + try { + // First look up a specific link for this message act_name + auto key = boost::make_tuple(authorizer_account, scope, act_name); + auto link = _db.find(key); + // If no specific link found, check for a contract-wide default + if (link == nullptr) { + boost::get<2>(key) = ""; + link = _db.find(key); + } + + // If no specific or default link found, use active permission + if (link != nullptr) { + return link->required_permission; + } + return optional(); + + // return optional(); + } FC_CAPTURE_AND_RETHROW((authorizer_account)(scope)(act_name)) + } + + optional authorization_manager::lookup_minimum_permission( account_name authorizer_account, + account_name scope, + action_name act_name + )const + { + // Special case native actions cannot be linked to a minimum permission, so there is no need to check. + if( scope == config::system_account_name ) { + FC_ASSERT( act_name != updateauth::get_name() && + act_name != deleteauth::get_name() && + act_name != linkauth::get_name() && + act_name != unlinkauth::get_name() && + act_name != canceldelay::get_name(), + "cannot call lookup_minimum_permission on native actions that are not allowed to be linked to minimum permissions" ); + } + + try { + optional linked_permission = lookup_linked_permission(authorizer_account, scope, act_name); + if( !linked_permission ) + return config::active_name; + + if( *linked_permission == config::eosio_any_name ) + return optional(); + + return linked_permission; + } FC_CAPTURE_AND_RETHROW((authorizer_account)(scope)(act_name)) + } + + optional authorization_manager::check_updateauth_authorization( const updateauth& update, + const vector& auths + )const + { + EOS_ASSERT( auths.size() == 1, tx_irrelevant_auth, + "updateauth action should only have one declared authorization" ); + const auto& auth = auths[0]; + EOS_ASSERT( auth.actor == update.account, tx_irrelevant_auth, + "the owner of the affected permission needs to be the actor of the declared authorization" ); + + const auto* min_permission = find_permission({update.account, update.permission}); + bool ignore_delay = false; + if( !min_permission ) { // creating a new permission + ignore_delay = true; + min_permission = &get_permission({update.account, update.parent}); + + } + const auto delay = get_permission(auth).satisfies( *min_permission, + _db.get_index().indices() ); + EOS_ASSERT( delay.valid(), + tx_irrelevant_auth, + "updateauth action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", auth)("min", permission_level{update.account, min_permission->name}) ); + + return (ignore_delay ? optional() : *delay); + } + + fc::microseconds authorization_manager::check_deleteauth_authorization( const deleteauth& del, + const vector& auths + )const + { + EOS_ASSERT( auths.size() == 1, tx_irrelevant_auth, + "deleteauth action should only have one declared authorization" ); + const auto& auth = auths[0]; + EOS_ASSERT( auth.actor == del.account, tx_irrelevant_auth, + "the owner of the permission to delete needs to be the actor of the declared authorization" ); + + const auto& min_permission = get_permission({del.account, del.permission}); + const auto delay = get_permission(auth).satisfies( min_permission, + _db.get_index().indices() ); + EOS_ASSERT( delay.valid(), + tx_irrelevant_auth, + "updateauth action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", auth)("min", permission_level{min_permission.owner, min_permission.name}) ); + + return *delay; + } + + fc::microseconds authorization_manager::check_linkauth_authorization( const linkauth& link, + const vector& auths + )const + { + EOS_ASSERT( auths.size() == 1, tx_irrelevant_auth, + "link action should only have one declared authorization" ); + const auto& auth = auths[0]; + EOS_ASSERT( auth.actor == link.account, tx_irrelevant_auth, + "the owner of the linked permission needs to be the actor of the declared authorization" ); + + EOS_ASSERT( link.type != updateauth::get_name(), action_validate_exception, + "Cannot link eosio::updateauth to a minimum permission" ); + EOS_ASSERT( link.type != deleteauth::get_name(), action_validate_exception, + "Cannot link eosio::deleteauth to a minimum permission" ); + EOS_ASSERT( link.type != linkauth::get_name(), action_validate_exception, + "Cannot link eosio::linkauth to a minimum permission" ); + EOS_ASSERT( link.type != unlinkauth::get_name(), action_validate_exception, + "Cannot link eosio::unlinkauth to a minimum permission" ); + EOS_ASSERT( link.type != canceldelay::get_name(), action_validate_exception, + "Cannot link eosio::canceldelay to a minimum permission" ); + + const auto linked_permission_name = lookup_minimum_permission(link.account, link.code, link.type); + + if( !linked_permission_name ) // if action is linked to eosio.any permission + return fc::microseconds(0); + + const auto delay = get_permission(auth).satisfies( get_permission({link.account, *linked_permission_name}), + _db.get_index().indices() ); + + EOS_ASSERT( delay.valid(), + tx_irrelevant_auth, + "link action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", auth)("min", permission_level{link.account, *linked_permission_name}) ); + + return *delay; + } + + fc::microseconds authorization_manager::check_unlinkauth_authorization( const unlinkauth& unlink, + const vector& auths + )const + { + EOS_ASSERT( auths.size() == 1, tx_irrelevant_auth, + "unlink action should only have one declared authorization" ); + const auto& auth = auths[0]; + EOS_ASSERT( auth.actor == unlink.account, tx_irrelevant_auth, + "the owner of the linked permission needs to be the actor of the declared authorization" ); + + const auto unlinked_permission_name = lookup_linked_permission(unlink.account, unlink.code, unlink.type); + EOS_ASSERT( unlinked_permission_name.valid(), transaction_exception, + "cannot unlink non-existent permission link of account '${account}' for actions matching '${code}::${action}'", + ("account", unlink.account)("code", unlink.code)("action", unlink.type) ); + + if( *unlinked_permission_name == config::eosio_any_name ) + return fc::microseconds(0); + + const auto delay = get_permission(auth).satisfies( get_permission({unlink.account, *unlinked_permission_name}), + _db.get_index().indices() ); + + EOS_ASSERT( delay.valid(), + tx_irrelevant_auth, + "unlink action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", auth)("min", permission_level{unlink.account, *unlinked_permission_name}) ); + + return *delay; + } + + void authorization_manager::check_canceldelay_authorization( const canceldelay& cancel, + const vector& auths + )const + { + EOS_ASSERT( auths.size() == 1, tx_irrelevant_auth, + "canceldelay action should only have one declared authorization" ); + const auto& auth = auths[0]; + + const auto delay = get_permission(auth).satisfies( get_permission(cancel.canceling_auth), + _db.get_index().indices() ); + EOS_ASSERT( delay.valid(), + tx_irrelevant_auth, + "canceldelay action declares irrelevant authority '${auth}'; specified authority to satisfy is ${min}", + ("auth", auth)("min", cancel.canceling_auth) ); + } + + class permission_visitor { + public: + permission_visitor( const authorization_manager& authorization ) + : _authorization(authorization), _track_delay(true) { + _max_delay_stack.emplace_back(); + } + + void operator()( const permission_level& perm_level ) { + const auto obj = _authorization.get_permission(perm_level); + if( _track_delay && _max_delay_stack.back() < obj.delay ) + _max_delay_stack.back() = obj.delay; + } + + void push_undo() { + _max_delay_stack.emplace_back( _max_delay_stack.back() ); + } + + void pop_undo() { + FC_ASSERT( _max_delay_stack.size() >= 2, "invariant failure in permission_visitor" ); + _max_delay_stack.pop_back(); + } + + void squash_undo() { + FC_ASSERT( _max_delay_stack.size() >= 2, "invariant failure in permission_visitor" ); + auto delay_to_keep = _max_delay_stack.back(); + _max_delay_stack.pop_back(); + _max_delay_stack.back() = delay_to_keep; + } + + fc::microseconds get_max_delay()const { + FC_ASSERT( _max_delay_stack.size() == 1, "invariant failure in permission_visitor" ); + return _max_delay_stack.back(); + } + + void pause_delay_tracking() { + _track_delay = false; + } + + void resume_delay_tracking() { + _track_delay = true; + } + + private: + const authorization_manager& _authorization; + vector _max_delay_stack; + bool _track_delay; + }; + + /** + * @param actions - the actions to check authorization across + * @param provided_keys - the set of public keys which have authorized the transaction + * @param allow_unused_signatures - true if method should not assert on unused signatures + * @param provided_accounts - the set of accounts which have authorized the transaction (presumed to be owner) + * + * @return fc::microseconds set to the max delay that this authorization requires to complete + */ + fc::microseconds authorization_manager::check_authorization( const vector& actions, + const flat_set& provided_keys, + bool allow_unused_signatures, + flat_set provided_accounts, + flat_set provided_levels + )const + { + auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; }, + permission_visitor(*this), + _control.get_global_properties().configuration.max_authority_depth, + provided_keys, provided_accounts, provided_levels ); + + fc::microseconds max_delay; + + for( const auto& act : actions ) { + bool special_case = false; + bool ignore_delay = false; + + if( act.account == config::system_account_name ) { + special_case = true; + + if( act.name == updateauth::get_name() ) { + const auto delay = check_updateauth_authorization(act.data_as(), act.authorization); + if( delay.valid() ) // update auth is used to modify an existing permission + max_delay = std::max( max_delay, *delay ); + else // updateauth is used to create a new permission + ignore_delay = true; + } else if( act.name == deleteauth::get_name() ) { + max_delay = std::max( max_delay, + check_deleteauth_authorization(act.data_as(), act.authorization) ); + } else if( act.name == linkauth::get_name() ) { + max_delay = std::max( max_delay, + check_linkauth_authorization(act.data_as(), act.authorization) ); + } else if( act.name == unlinkauth::get_name() ) { + max_delay = std::max( max_delay, + check_unlinkauth_authorization(act.data_as(), act.authorization) ); + } else if( act.name == canceldelay::get_name() ) { + check_canceldelay_authorization(act.data_as(), act.authorization); + ignore_delay = true; + } else { + special_case = false; + } + } + + for( const auto& declared_auth : act.authorization ) { + + if( !special_case ) { + auto min_permission_name = lookup_minimum_permission(declared_auth.actor, act.account, act.name); + if( min_permission_name ) { // since special cases were already handled, it should only be false if the permission is eosio.any + const auto& min_permission = get_permission({declared_auth.actor, *min_permission_name}); + auto delay = get_permission(declared_auth).satisfies( min_permission, + _db.get_index().indices() ); + EOS_ASSERT( delay.valid(), + tx_irrelevant_auth, + "action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", declared_auth)("min", permission_level{min_permission.owner, min_permission.name}) ); + max_delay = std::max( max_delay, *delay ); + } + } + + //if( should_check_signatures() ) { + if( ignore_delay ) + checker.get_permission_visitor().pause_delay_tracking(); + EOS_ASSERT(checker.satisfied(declared_auth), tx_missing_sigs, + "transaction declares authority '${auth}', but does not have signatures for it.", + ("auth", declared_auth)); + if( ignore_delay ) + checker.get_permission_visitor().resume_delay_tracking(); + //} + } + } + + if( !allow_unused_signatures ) { //&& should_check_signatures() ) { + EOS_ASSERT( checker.all_keys_used(), tx_irrelevant_sig, + "transaction bears irrelevant signatures from these keys: ${keys}", + ("keys", checker.unused_keys()) ); + } + + const auto checker_max_delay = checker.get_permission_visitor().get_max_delay(); + + return std::max(max_delay, checker_max_delay); + } + + /** + * @param account - the account owner of the permission + * @param permission - the permission name to check for authorization + * @param provided_keys - a set of public keys + * + * @return true if the provided keys are sufficient to authorize the account permission + */ + bool authorization_manager::check_authorization( account_name account, permission_name permission, + flat_set provided_keys, + bool allow_unused_signatures + )const + { + auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; }, + noop_permission_visitor(), + _control.get_global_properties().configuration.max_authority_depth, + provided_keys); + + auto satisfied = checker.satisfied({account, permission}); + + if( satisfied && !allow_unused_signatures ) { + EOS_ASSERT(checker.all_keys_used(), tx_irrelevant_sig, + "irrelevant signatures from these keys: ${keys}", + ("keys", checker.unused_keys())); + } + + return satisfied; + } + + flat_set authorization_manager::get_required_keys( const transaction& trx, + const flat_set& candidate_keys + )const + { + auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; }, + noop_permission_visitor(), + _control.get_global_properties().configuration.max_authority_depth, + candidate_keys); + + for (const auto& act : trx.actions ) { + for (const auto& declared_auth : act.authorization) { + if (!checker.satisfied(declared_auth)) { + EOS_ASSERT(checker.satisfied(declared_auth), tx_missing_sigs, + "transaction declares authority '${auth}', but does not have signatures for it.", + ("auth", declared_auth)); + } + } + } + + return checker.used_keys(); + } + +} } /// namespace eosio::chain diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index da4f7da09029f513b72cc49aa242a8cf832f63e4..ed20af5c0ce2b803ccb51ac0788a431e2109c973 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -9,11 +9,10 @@ #include #include #include -#include #include #include -#include +#include #include #include @@ -51,6 +50,7 @@ struct controller_impl { fork_database fork_db; wasm_interface wasmif; resource_limits_manager resource_limits; + authorization_manager authorization; controller::config conf; controller& self; @@ -94,6 +94,7 @@ struct controller_impl { fork_db( cfg.shared_memory_dir ), wasmif( cfg.wasm_runtime ), resource_limits( db ), + authorization( s ), conf( cfg ), self( s ) { @@ -120,11 +121,26 @@ struct controller_impl { void init() { // ilog( "${c}", ("c",fc::json::to_pretty_string(cfg)) ); - initialize_indicies(); + add_indices(); - initialize_fork_db(); /** - * The undable state contains state transitions from blocks + * The fork database needs an initial block_state to be set before + * it can accept any new blocks. This initial block state can be found + * in the database (whose head block state should be irreversible) or + * it would be the genesis state. + */ + if( !head ) { + initialize_fork_db(); // set head to genesis state +#warning What if head is empty because the user deleted forkdb.dat? Will this not corrupt the database? + db.set_revision( head->block_num ); + initialize_database(); + } + + FC_ASSERT( db.revision() == head->block_num, "fork database is inconsistent with shared memory", + ("db",db.revision())("head",head->block_num) ); + + /** + * The undoable state contains state transitions from blocks * in the fork database that could be reversed. Because this * is a new startup and the fork database is empty, we must * unwind that pending state. This state will be regenerated @@ -141,13 +157,9 @@ struct controller_impl { db.flush(); } - void initialize_indicies() { + void add_indices() { db.add_index(); db.add_index(); - db.add_index(); - db.add_index(); - db.add_index(); - db.add_index(); db.add_index(); db.add_index(); @@ -163,7 +175,8 @@ struct controller_impl { db.add_index(); db.add_index(); - resource_limits.initialize_database(); + authorization.add_indices(); + resource_limits.add_indices(); } void abort_pending_block() { @@ -183,47 +196,36 @@ struct controller_impl { } /** - * The fork database needs an initial block_state to be set before - * it can accept any new blocks. This initial block state can be found - * in the database (whose head block state should be irreversible) or - * it would be the genesis state. + * Sets fork database head to the genesis state. */ void initialize_fork_db() { - head = fork_db.head(); - if( !head ) { - wlog( " Initializing new blockchain with genesis state " ); - producer_schedule_type initial_schedule{ 0, {{N(eosio), conf.genesis.initial_key}} }; + wlog( " Initializing new blockchain with genesis state " ); + producer_schedule_type initial_schedule{ 0, {{N(eosio), conf.genesis.initial_key}} }; - block_header_state genheader; - genheader.active_schedule = initial_schedule; - genheader.pending_schedule = initial_schedule; - genheader.pending_schedule_hash = fc::sha256::hash(initial_schedule); - genheader.header.timestamp = conf.genesis.initial_timestamp; - genheader.header.action_mroot = conf.genesis.compute_chain_id(); - genheader.id = genheader.header.id(); - genheader.block_num = genheader.header.block_num(); + block_header_state genheader; + genheader.active_schedule = initial_schedule; + genheader.pending_schedule = initial_schedule; + genheader.pending_schedule_hash = fc::sha256::hash(initial_schedule); + genheader.header.timestamp = conf.genesis.initial_timestamp; + genheader.header.action_mroot = conf.genesis.compute_chain_id(); + genheader.id = genheader.header.id(); + genheader.block_num = genheader.header.block_num(); - head = std::make_shared( genheader ); - signed_block genblock(genheader.header); + head = std::make_shared( genheader ); + signed_block genblock(genheader.header); - edump((genheader.header)); - edump((genblock)); - blog.append( genblock ); + edump((genheader.header)); + edump((genblock)); + blog.append( genblock ); - db.set_revision( head->block_num ); - fork_db.set( head ); - - initialize_database(); - } - FC_ASSERT( db.revision() == head->block_num, "fork database is inconsistant with shared memory", - ("db",db.revision())("head",head->block_num) ); + fork_db.set( head ); } - void create_native_account( account_name name ) { + void create_native_account( account_name name, const authority& owner, const authority& active, bool is_privileged = false ) { db.create([&](auto& a) { a.name = name; a.creation_date = conf.genesis.initial_timestamp; - a.privileged = true; + a.privileged = is_privileged; if( name == config::system_account_name ) { a.set_abi(eosio_contract_abi(abi_def())); @@ -233,21 +235,20 @@ struct controller_impl { a.name = name; }); - const auto& owner = db.create([&](auto& p) { - p.owner = name; - p.name = "owner"; - p.auth.threshold = 1; - p.auth.keys.push_back( key_weight{ .key = conf.genesis.initial_key, .weight = 1 } ); - }); - db.create([&](auto& p) { - p.owner = name; - p.parent = owner.id; - p.name = "active"; - p.auth.threshold = 1; - p.auth.keys.push_back( key_weight{ .key = conf.genesis.initial_key, .weight = 1 } ); - }); + const auto& owner_permission = authorization.create_permission(name, config::owner_name, 0, + owner, conf.genesis.initial_timestamp ); + const auto& active_permission = authorization.create_permission(name, config::active_name, owner_permission.id, + active, conf.genesis.initial_timestamp ); resource_limits.initialize_account(name); + resource_limits.add_pending_account_ram_usage( + name, + (int64_t)(config::billable_size_v + owner_permission.auth.get_billable_size()) + ); + resource_limits.add_pending_account_ram_usage( + name, + (int64_t)(config::billable_size_v + active_permission.auth.get_billable_size()) + ); } void initialize_database() { @@ -255,53 +256,30 @@ struct controller_impl { for (int i = 0; i < 0x10000; i++) db.create([&](block_summary_object&) {}); + const auto& tapos_block_summary = db.get(1); + db.modify( tapos_block_summary, [&]( auto& bs ) { + bs.block_id = head->id; + }); + db.create([&](auto& gpo ){ gpo.configuration = conf.genesis.initial_configuration; }); db.create([](auto&){}); - db.create([](auto&){}); /// reserve perm 0 (used else where) - - resource_limits.initialize_chain(); - - create_native_account( config::system_account_name ); - - // Create special accounts - auto create_special_account = [&](account_name name, const auto& owner, const auto& active) { - db.create([&](account_object& a) { - a.name = name; - a.creation_date = conf.genesis.initial_timestamp; - }); - db.create([&](auto & a) { - a.name = name; - }); - const auto& owner_permission = db.create([&](permission_object& p) { - p.name = config::owner_name; - p.parent = 0; - p.owner = name; - p.auth = move(owner); - }); - db.create([&](permission_object& p) { - p.name = config::active_name; - p.parent = owner_permission.id; - p.owner = owner_permission.owner; - p.auth = move(active); - }); - }; + + authorization.initialize_database(); + resource_limits.initialize_database(); + + authority system_auth(conf.genesis.initial_key); + create_native_account( config::system_account_name, system_auth, system_auth, true ); auto empty_authority = authority(0, {}, {}); auto active_producers_authority = authority(0, {}, {}); active_producers_authority.accounts.push_back({{config::system_account_name, config::active_name}, 1}); - create_special_account(config::nobody_account_name, empty_authority, empty_authority); - create_special_account(config::producers_account_name, empty_authority, active_producers_authority); - - const auto& tapos_block_summary = db.get(1); - db.modify( tapos_block_summary, [&]( auto& bs ) { - bs.block_id = head->id; - }); + create_native_account( config::nobody_account_name, empty_authority, empty_authority ); + create_native_account( config::producers_account_name, empty_authority, active_producers_authority ); } - void set_pending_tapos() { const auto& tapos_block_summary = db.get((uint16_t)pending->_pending_block_state->block_num); db.modify( tapos_block_summary, [&]( auto& bs ) { @@ -636,6 +614,14 @@ resource_limits_manager& controller::get_mutable_resource_limits_manager return my->resource_limits; } +const authorization_manager& controller::get_authorization_manager()const +{ + return my->authorization; +} +authorization_manager& controller::get_mutable_authorization_manager() +{ + return my->authorization; +} controller::controller( const controller::config& cfg ) :my( new controller_impl( cfg, *this ) ) @@ -789,36 +775,6 @@ void controller::pop_block() { } -/** - * @param actions - the actions to check authorization across - * @param provided_keys - the set of public keys which have authorized the transaction - * @param allow_unused_signatures - true if method should not assert on unused signatures - * @param provided_accounts - the set of accounts which have authorized the transaction (presumed to be owner) - * - * @return fc::microseconds set to the max delay that this authorization requires to complete - */ -fc::microseconds controller::check_authorization( const vector& actions, - const flat_set& provided_keys, - bool allow_unused_signatures, - flat_set provided_accounts, - flat_set provided_levels - )const { - return fc::microseconds(); -} - -/** - * @param account - the account owner of the permission - * @param permission - the permission name to check for authorization - * @param provided_keys - a set of public keys - * - * @return true if the provided keys are sufficient to authorize the account permission - */ -bool controller::check_authorization( account_name account, permission_name permission, - flat_set provided_keys, - bool allow_unused_signatures)const { - return true; -} - void controller::set_active_producers( const producer_schedule_type& sch ) { FC_ASSERT( !my->pending->_pending_block_state->header.new_producers, "this block has already set new producers" ); FC_ASSERT( !my->pending->_pending_block_state->pending_schedule.producers.size(), "there is already a pending schedule, wait for it to become active" ); diff --git a/libraries/chain/eosio_contract.cpp b/libraries/chain/eosio_contract.cpp index e80eac3b44e43ad9f6d33f1a7463320f6f8976e5..9770dc6bf064f3e5b45fda8c1fd6ef12afd2c944 100644 --- a/libraries/chain/eosio_contract.cpp +++ b/libraries/chain/eosio_contract.cpp @@ -21,9 +21,10 @@ #include #include +#include #include -namespace eosio { namespace chain { +namespace eosio { namespace chain { @@ -35,7 +36,7 @@ uint128_t transaction_id_to_sender_id( const transaction_id_type& tid ) { void validate_authority_precondition( const apply_context& context, const authority& auth ) { for(const auto& a : auth.accounts) { context.db.get(a.permission.actor); - context.db.get(boost::make_tuple(a.permission.actor, a.permission.permission)); + context.mutable_controller.get_authorization_manager().get_permission({a.permission.actor, a.permission.permission}); } } @@ -47,7 +48,8 @@ void apply_eosio_newaccount(apply_context& context) { try { context.require_authorization(create.creator); // context.require_write_lock( config::eosio_auth_scope ); - auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); + auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); + auto& authorization = context.mutable_controller.get_mutable_authorization_manager(); EOS_ASSERT( validate(create.owner), action_validate_exception, "Invalid owner authority"); EOS_ASSERT( validate(create.active), action_validate_exception, "Invalid active authority"); @@ -85,32 +87,25 @@ void apply_eosio_newaccount(apply_context& context) { a.name = create.name; }); - resources.initialize_account(create.name); + const auto& owner_permission = authorization.create_permission( create.name, config::owner_name, 0, + std::move(create.owner) ); + const auto& active_permission = authorization.create_permission( create.name, config::active_name, owner_permission.id, + std::move(create.active) ); + resources.add_pending_account_ram_usage( create.creator, (int64_t)config::overhead_per_account_ram_bytes ); - auto create_permission = [owner=create.name, &db, &context, &resources](const permission_name& name, permission_object::id_type parent, authority &&auth) { - const auto& result = db.create([&](permission_object& p) { - p.name = name; - p.parent = parent; - p.owner = owner; - p.auth = std::move(auth); - }); - - resources.add_pending_account_ram_usage( - owner, - (int64_t)(config::billable_size_v + result.auth.get_billable_size()) - ); - - return result; - }; - - // If a parent_id of 0 is going to be used to indicate the absence of a parent, then we need to make sure that the chain - // initializes permission_index with a dummy object that reserves the id of 0. - const auto& owner_permission = create_permission(config::owner_name, 0, std::move(create.owner)); - create_permission(config::active_name, owner_permission.id, std::move(create.active)); + resources.initialize_account(create.name); + resources.add_pending_account_ram_usage( + create.name, + (int64_t)(config::billable_size_v + owner_permission.auth.get_billable_size()) + ); + resources.add_pending_account_ram_usage( + create.name, + (int64_t)(config::billable_size_v + active_permission.auth.get_billable_size()) + ); } FC_CAPTURE_AND_RETHROW( (create) ) } @@ -191,12 +186,15 @@ void apply_eosio_setabi(apply_context& context) { } void apply_eosio_updateauth(apply_context& context) { - auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); // context.require_write_lock( config::eosio_auth_scope ); + auto update = context.act.data_as(); + context.require_authorization(update.account); // only here to mark the single authority on this action as used + + auto& authorization = context.mutable_controller.get_mutable_authorization_manager(); + auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); auto& db = context.db; - auto update = context.act.data_as(); EOS_ASSERT(!update.permission.empty(), action_validate_exception, "Cannot create authority with empty name"); EOS_ASSERT( update.permission.to_string().find( "eosio." ) != 0, action_validate_exception, "Permission names that start with 'eosio.' are reserved" ); @@ -211,54 +209,25 @@ void apply_eosio_updateauth(apply_context& context) { else EOS_ASSERT(!update.parent.empty(), action_validate_exception, "Only owner permission can have empty parent" ); - 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; - } - const permission_object *current = db.find(boost::make_tuple(update.account, update.permission)); - // Permission doesn't exist yet, check parent permission - if (current == nullptr) current = db.find(boost::make_tuple(update.account, update.parent)); - // Ensure either the permission or parent's permission exists - EOS_ASSERT(current != nullptr, permission_query_exception, - "Failed to retrieve permission for: {\"actor\": \"${actor}\", \"permission\": \"${permission}\" }", - ("actor", update.account)("permission", update.parent)); - - 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"); - validate_authority_precondition(context, update.auth); - auto permission = db.find(boost::make_tuple(update.account, update.permission)); + auto permission = authorization.find_permission({update.account, update.permission}); // If a parent_id of 0 is going to be used to indicate the absence of a parent, then we need to make sure that the chain // initializes permission_index with a dummy object that reserves the id of 0. - permission_object::id_type parent_id = 0; - if(update.permission != config::owner_name) { - auto& parent = db.get(boost::make_tuple(update.account, update.parent)); + authorization_manager::permission_id_type parent_id = 0; + if( update.permission != config::owner_name ) { + auto& parent = authorization.get_permission({update.account, update.parent}); parent_id = parent.id; } - if (permission) { + if( permission ) { EOS_ASSERT(parent_id == permission->parent, action_validate_exception, "Changing parent authority is not currently supported"); int64_t old_size = (int64_t)(config::billable_size_v + permission->auth.get_billable_size()); - // TODO/QUESTION: If we are updating an existing permission, should we check if the message declared - // permission satisfies the permission we want to modify? db.modify(*permission, [&update, &parent_id, &context](permission_object& po) { po.auth = update.auth; po.parent = parent_id; @@ -273,38 +242,29 @@ void apply_eosio_updateauth(apply_context& context) { new_size - old_size ); } else { - // TODO/QUESTION: If we are creating a new permission, should we check if the message declared - // permission satisfies the parent permission? - const auto& p = db.create([&update, &parent_id, &context](permission_object& po) { - po.name = update.permission; - po.owner = update.account; - po.auth = update.auth; - po.parent = parent_id; - po.last_updated = context.control.head_block_time(); - po.delay = fc::seconds(update.auth.delay_sec); - }); + const auto& p = authorization.create_permission( update.account, update.permission, parent_id, update.auth ); resources.add_pending_account_ram_usage( - p.owner, + update.account, (int64_t)(config::billable_size_v + p.auth.get_billable_size()) ); - } } void apply_eosio_deleteauth(apply_context& context) { - auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); +// context.require_write_lock( config::eosio_auth_scope ); + auto remove = context.act.data_as(); + context.require_authorization(remove.account); // only here to mark the single authority on this action as used + EOS_ASSERT(remove.permission != config::active_name, action_validate_exception, "Cannot delete active authority"); EOS_ASSERT(remove.permission != config::owner_name, action_validate_exception, "Cannot delete owner authority"); + auto& authorization = context.mutable_controller.get_authorization_manager(); + auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); auto& db = context.db; - context.require_authorization(remove.account); - // TODO/QUESTION: - // Inconsistency between permissions that can be satisfied to create/modify (via updateauth) a permission and the - // stricter requirements for deleting the permission using deleteauth. - // If a permission can be updated, shouldn't it also be allowed to delete it without higher permissions required? - const auto& permission = db.get(boost::make_tuple(remove.account, remove.permission)); + + const auto& permission = authorization.get_permission({remove.account, remove.permission}); { // Check for children const auto& index = db.get_index(); @@ -328,12 +288,14 @@ void apply_eosio_deleteauth(apply_context& context) { } void apply_eosio_linkauth(apply_context& context) { +// context.require_write_lock( config::eosio_auth_scope ); + auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); auto requirement = context.act.data_as(); try { EOS_ASSERT(!requirement.requirement.empty(), action_validate_exception, "Required permission cannot be empty"); - context.require_authorization(requirement.account); + context.require_authorization(requirement.account); // only here to mark the single authority on this action as used auto& db = context.db; const auto *account = db.find(requirement.account); @@ -374,11 +336,13 @@ void apply_eosio_linkauth(apply_context& context) { } void apply_eosio_unlinkauth(apply_context& context) { +// context.require_write_lock( config::eosio_auth_scope ); + auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); auto& db = context.db; auto unlink = context.act.data_as(); - context.require_authorization(unlink.account); + context.require_authorization(unlink.account); // only here to mark the single authority on this action as used auto link_key = boost::make_tuple(unlink.account, unlink.code, unlink.type); auto link = db.find(link_key); @@ -391,7 +355,6 @@ void apply_eosio_unlinkauth(apply_context& context) { db.remove(*link); } - void apply_eosio_onerror(apply_context& context) { FC_ASSERT(context.sender != account_name(), "onerror action cannot be called directly"); context.require_recipient(context.sender); @@ -567,6 +530,8 @@ void apply_eosio_vetorecovery(apply_context& context) { void apply_eosio_canceldelay(apply_context& context) { auto cancel = context.act.data_as(); + context.require_authorization(cancel.canceling_auth.actor); // only here to mark the single authority on this action as used + const auto& trx_id = cancel.trx_id; const auto& generated_transaction_idx = context.control.db().get_index(); @@ -576,23 +541,18 @@ void apply_eosio_canceldelay(apply_context& context) { "cannot cancel trx_id=${tid}, there is no deferred transaction with that transaction id",("tid", trx_id)); auto dtrx = fc::raw::unpack(itr->packed_trx.data(), itr->packed_trx.size()); - set accounts; - for (const auto& act : dtrx.actions) { - for (const auto& auth : act.authorization) { - accounts.insert(auth.actor); - } - } - bool found = false; - for (const auto& auth : context.act.authorization) { - if (auth.permission == config::active_name && accounts.count(auth.actor)) { - found = true; - break; + for( const auto& act : dtrx.actions ) { + for( const auto& auth : act.authorization ) { + if( auth == cancel.canceling_auth ) { + found = true; + break; + } } + if( found ) break; } - FC_ASSERT (found, "canceldelay action must be signed with the \"active\" permission for one of the actors" - " provided in the authorizations on the original transaction"); + FC_ASSERT (found, "canceling_auth in canceldelay action was not found as authorization in the original delayed transaction"); context.cancel_deferred_transaction(transaction_id_to_sender_id(trx_id)); } diff --git a/libraries/chain/eosio_contract_abi.cpp b/libraries/chain/eosio_contract_abi.cpp index c1d899a76473d48d54490868fb83262eb7832bcb..deae0627ed1706292ea8e6b0d40f9d0481b28cca 100644 --- a/libraries/chain/eosio_contract_abi.cpp +++ b/libraries/chain/eosio_contract_abi.cpp @@ -28,8 +28,8 @@ abi_def eosio_contract_abi(const abi_def& eosio_system_abi) eos_abi.actions.push_back( action_def{name("onerror"), "onerror",""} ); eos_abi.actions.push_back( action_def{name("onblock"), "onblock",""} ); eos_abi.actions.push_back( action_def{name("canceldelay"), "canceldelay",""} ); - - // TODO add any ricardian_clauses + + // TODO add any ricardian_clauses // // ACTION PAYLOADS @@ -116,6 +116,7 @@ abi_def eosio_contract_abi(const abi_def& eosio_system_abi) eos_abi.structs.emplace_back( struct_def { "canceldelay", "", { + {"canceling_auth", "permission_level"}, {"trx_id", "transaction_id_type"}, } }); @@ -223,7 +224,7 @@ abi_def eosio_contract_abi(const abi_def& eosio_system_abi) eos_abi.structs.emplace_back( struct_def { "clause_pair", "", { {"id", "string"}, - {"body", "string"} + {"body", "string"} } }); eos_abi.structs.emplace_back( struct_def { diff --git a/libraries/chain/include/eosio/chain/action_objects.hpp b/libraries/chain/include/eosio/chain/action_objects.hpp deleted file mode 100644 index 4a0e9e2f5c2ddcdec8f8d28e0ef7bdb6ae52f02f..0000000000000000000000000000000000000000 --- a/libraries/chain/include/eosio/chain/action_objects.hpp +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @file - * @copyright defined in eos/LICENSE.txt - */ -#pragma once -#include -#include - -#include "multi_index_includes.hpp" - -namespace eosio { namespace chain { - - /** - * Maps the permission level on the code to the permission level specififed by owner, when specifying a contract the - * contract will specify 1 permission_object per action, and by default the parent of that permission object will be - * the active permission of the contract; however, the contract owner could group their actions any way they like. - * - * When it comes time to evaluate whether User can call Action on Contract with UserPermissionLevel the algorithm - * operates as follows: - * - * let scope_permission = action_code.permission - * while( ! mapping for (scope_permission / owner ) - * scope_permission = scope_permission.parent - * if( !scope_permission ) - * user permission => active - * break; - * - * Now that we know the required user permission... - * - * while( ! transaction.has( user_permission ) ) - * user_permission = user_permission.parent - * if( !user_permission ) - * throw invalid permission - * pass - */ - class action_permission_object : public chainbase::object - { - OBJECT_CTOR(action_permission_object) - - id_type id; - account_name owner; ///< the account whose permission we seek - permission_object::id_type scope_permission; ///< the scope permission defined by the contract for the action - permission_object::id_type owner_permission; ///< the owner permission that is required - }; - - struct by_owner_scope; - using action_permission_index = chainbase::shared_multi_index_container< - action_permission_object, - indexed_by< - ordered_unique, member>, - ordered_unique, - composite_key< action_permission_object, - member, - member - > - > - > - >; - -} } // eosio::chain - -CHAINBASE_SET_INDEX_TYPE(eosio::chain::action_permission_object, eosio::chain::action_permission_index) - -FC_REFLECT(eosio::chain::action_permission_object, (id)(owner)(owner_permission)(scope_permission) ) diff --git a/libraries/chain/include/eosio/chain/authority_checker.hpp b/libraries/chain/include/eosio/chain/authority_checker.hpp index 031ec1ee34a38927964c52f0b2707dbf7d70e1c2..1ed307a09d15c3b9888add489e6ce648f3b302f4 100644 --- a/libraries/chain/include/eosio/chain/authority_checker.hpp +++ b/libraries/chain/include/eosio/chain/authority_checker.hpp @@ -170,7 +170,7 @@ namespace detail { return {range.begin(), range.end()}; } - const PermissionVisitorFunc& get_permission_visitor() { + PermissionVisitorFunc& get_permission_visitor() { return permission_visitor; } diff --git a/libraries/chain/include/eosio/chain/authorization_manager.hpp b/libraries/chain/include/eosio/chain/authorization_manager.hpp new file mode 100644 index 0000000000000000000000000000000000000000..1b5794488407c34b1208e232ac3c8aa8684041c4 --- /dev/null +++ b/libraries/chain/include/eosio/chain/authorization_manager.hpp @@ -0,0 +1,106 @@ +/** + * @file + * @copyright defined in eos/LICENSE.txt + */ +#pragma once + +#include +#include + +namespace eosio { namespace chain { + + class controller; + struct updateauth; + struct deleteauth; + struct linkauth; + struct unlinkauth; + struct canceldelay; + + class authorization_manager { + public: + using permission_id_type = permission_object::id_type; + + explicit authorization_manager(controller& c); + + void add_indices(); + void initialize_database(); + + const permission_object& create_permission( account_name account, + permission_name name, + permission_id_type parent, + const authority& auth, + time_point initial_creation_time = time_point() + ); + + const permission_object& create_permission( account_name account, + permission_name name, + permission_id_type parent, + authority&& auth, + time_point initial_creation_time = time_point() + ); + + const permission_object* find_permission( const permission_level& level )const; + const permission_object& get_permission( const permission_level& level )const; + + /** + * @brief Find the lowest authority level required for @ref authorizer_account to authorize a message of the + * specified type + * @param authorizer_account The account authorizing the message + * @param code_account The account which publishes the contract that handles the message + * @param type The type of message + */ + optional lookup_minimum_permission( account_name authorizer_account, + scope_name code_account, + action_name type + )const; + + /** + * @param actions - the actions to check authorization across + * @param provided_keys - the set of public keys which have authorized the transaction + * @param allow_unused_signatures - true if method should not assert on unused signatures + * @param provided_accounts - the set of accounts which have authorized the transaction (presumed to be owner) + * + * @return fc::microseconds set to the max delay that this authorization requires to complete + */ + fc::microseconds check_authorization( const vector& actions, + const flat_set& provided_keys, + bool allow_unused_signatures = false, + flat_set provided_accounts = flat_set(), + flat_set provided_levels = flat_set() + )const; + + /** + * @param account - the account owner of the permission + * @param permission - the permission name to check for authorization + * @param provided_keys - a set of public keys + * + * @return true if the provided keys are sufficient to authorize the account permission + */ + bool check_authorization( account_name account, permission_name permission, + flat_set provided_keys, + bool allow_unused_signatures + )const; + + flat_set get_required_keys( const transaction& trx, + const flat_set& candidate_keys + )const; + + + + private: + const controller& _control; + chainbase::database& _db; + + optional check_updateauth_authorization( const updateauth& update, const vector& auths )const; + fc::microseconds check_deleteauth_authorization( const deleteauth& del, const vector& auths )const; + fc::microseconds check_linkauth_authorization( const linkauth& link, const vector& auths )const; + fc::microseconds check_unlinkauth_authorization( const unlinkauth& unlink, const vector& auths )const; + void check_canceldelay_authorization( const canceldelay& cancel, const vector& auths )const; + + optional lookup_linked_permission( account_name authorizer_account, + scope_name code_account, + action_name type + )const; + }; + +} } /// namespace eosio::chain diff --git a/libraries/chain/include/eosio/chain/contract_types.hpp b/libraries/chain/include/eosio/chain/contract_types.hpp index d0f38c5dbe913631a6840cc462a60f01896f6ee0..93e2ef6094ae2409c40b05ea7d09f3703c76058a 100644 --- a/libraries/chain/include/eosio/chain/contract_types.hpp +++ b/libraries/chain/include/eosio/chain/contract_types.hpp @@ -7,7 +7,7 @@ #include -namespace eosio { namespace chain { +namespace eosio { namespace chain { using fixed_string32 = fc::fixed_string>; using fixed_string16 = fc::fixed_string<>; @@ -63,7 +63,7 @@ struct action_def { action_name name; type_name type; - string ricardian_contract; + string ricardian_contract; }; struct table_def { @@ -82,7 +82,7 @@ struct table_def { struct clause_pair { clause_pair() = default; clause_pair( const string& id, const string& body ) - : id(id), body(body) + : id(id), body(body) {} string id; @@ -271,6 +271,7 @@ struct vetorecovery { }; struct canceldelay { + permission_level canceling_auth; transaction_id_type trx_id; static account_name get_account() { @@ -302,4 +303,4 @@ FC_REFLECT( eosio::chain::unlinkauth , (account)(code)(typ FC_REFLECT( eosio::chain::postrecovery , (account)(auth)(memo) ) FC_REFLECT( eosio::chain::passrecovery , (account) ) FC_REFLECT( eosio::chain::vetorecovery , (account) ) -FC_REFLECT( eosio::chain::canceldelay , (trx_id) ) +FC_REFLECT( eosio::chain::canceldelay , (canceling_auth)(trx_id) ) diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index 307a09089c45ff412fa590ef85ee15b06383df75..f5379be2578e88099de748bc3492beff98bc3e80 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -11,6 +11,8 @@ namespace chainbase { namespace eosio { namespace chain { + class authorization_manager; + namespace resource_limits { class resource_limits_manager; }; @@ -87,7 +89,7 @@ namespace eosio { namespace chain { void commit_block(); void log_irreversible_blocks(); void pop_block(); - + void push_block( const signed_block_ptr& b ); chainbase::database& db()const; @@ -108,6 +110,8 @@ namespace eosio { namespace chain { const permission_object& get_permission( const permission_level& level )const; const resource_limits_manager& get_resource_limits_manager()const; resource_limits_manager& get_mutable_resource_limits_manager(); + const authorization_manager& get_authorization_manager()const; + authorization_manager& get_mutable_authorization_manager(); block_id_type head_block_id()const; account_name head_block_producer()const; @@ -121,32 +125,6 @@ namespace eosio { namespace chain { signed_block_ptr fetch_block_by_number( uint32_t block_num )const; signed_block_ptr fetch_block_by_id( block_id_type id )const; - /** - * @param actions - the actions to check authorization across - * @param provided_keys - the set of public keys which have authorized the transaction - * @param allow_unused_signatures - true if method should not assert on unused signatures - * @param provided_accounts - the set of accounts which have authorized the transaction (presumed to be owner) - * - * @return fc::microseconds set to the max delay that this authorization requires to complete - */ - fc::microseconds check_authorization( const vector& actions, - const flat_set& provided_keys, - bool allow_unused_signatures = false, - flat_set provided_accounts = flat_set(), - flat_set provided_levels = flat_set() - )const; - - /** - * @param account - the account owner of the permission - * @param permission - the permission name to check for authorization - * @param provided_keys - a set of public keys - * - * @return true if the provided keys are sufficient to authorize the account permission - */ - bool check_authorization( account_name account, permission_name permission, - flat_set provided_keys, - bool allow_unused_signatures )const; - void validate_referenced_accounts( const transaction& t )const; void validate_expiration( const transaction& t )const; void validate_tapos( const transaction& t )const; @@ -180,9 +158,9 @@ namespace eosio { namespace chain { } } /// eosio::chain FC_REFLECT( eosio::chain::controller::config::runtime_limits, (max_push_block_us)(max_push_transaction_us)(max_deferred_transactions_us) ) -FC_REFLECT( eosio::chain::controller::config, +FC_REFLECT( eosio::chain::controller::config, (block_log_dir) (shared_memory_dir)(shared_memory_size)(read_only) (genesis) - (limits)(wasm_runtime) + (limits)(wasm_runtime) ) diff --git a/libraries/chain/include/eosio/chain/resource_limits.hpp b/libraries/chain/include/eosio/chain/resource_limits.hpp index 02f33aa927558e2ccb24d518a66b28e3d69617cf..9268f0687b5b284bab211f9fc3bbed6aa1568a47 100644 --- a/libraries/chain/include/eosio/chain/resource_limits.hpp +++ b/libraries/chain/include/eosio/chain/resource_limits.hpp @@ -31,8 +31,8 @@ namespace eosio { namespace chain { namespace resource_limits { { } + void add_indices(); void initialize_database(); - void initialize_chain(); void initialize_account( const account_name& account ); void set_block_parameters( const elastic_limit_parameters& cpu_limit_parameters, const elastic_limit_parameters& net_limit_parameters ); @@ -61,4 +61,3 @@ namespace eosio { namespace chain { namespace resource_limits { chainbase::database& _db; }; } } } /// eosio::chain - diff --git a/libraries/chain/include/eosio/chain/types.hpp b/libraries/chain/include/eosio/chain/types.hpp index aafa88e352f8bf513397db0f3f801335986dfaf0..e97f734d613693aa63e1ed6c5ab01e514a60988a 100644 --- a/libraries/chain/include/eosio/chain/types.hpp +++ b/libraries/chain/include/eosio/chain/types.hpp @@ -125,7 +125,6 @@ namespace eosio { namespace chain { index128_object_type, index256_object_type, index_double_object_type, - action_permission_object_type, global_property_object_type, dynamic_global_property_object_type, block_summary_object_type, @@ -184,7 +183,6 @@ FC_REFLECT_ENUM(eosio::chain::object_type, (index128_object_type) (index256_object_type) (index_double_object_type) - (action_permission_object_type) (global_property_object_type) (dynamic_global_property_object_type) (block_summary_object_type) diff --git a/libraries/chain/resource_limits.cpp b/libraries/chain/resource_limits.cpp index 7edc67ebbfe4b724d7a65199754fd603d6fc1afe..a3d09c0d9afa9d356a34da45a0f2e6998e615194 100644 --- a/libraries/chain/resource_limits.cpp +++ b/libraries/chain/resource_limits.cpp @@ -26,14 +26,14 @@ void resource_limits_state_object::update_virtual_net_limit( const resource_limi virtual_net_limit = update_elastic_limit(virtual_net_limit, average_block_net_usage.average(), cfg.net_limit_parameters); } -void resource_limits_manager::initialize_database() { +void resource_limits_manager::add_indices() { _db.add_index(); _db.add_index(); _db.add_index(); _db.add_index(); } -void resource_limits_manager::initialize_chain() { +void resource_limits_manager::initialize_database() { const auto& config = _db.create([](resource_limits_config_object& config){ // see default settings in the declaration }); @@ -134,9 +134,9 @@ void resource_limits_manager::add_pending_account_ram_usage( const account_name FC_ASSERT( usage.ram_usage >= 0, "cannot have negative usage!" ); - EOS_ASSERT(ram_delta < 0 || UINT64_MAX - usage.ram_usage>= (uint64_t)ram_delta, transaction_exception, + EOS_ASSERT(ram_delta < 0 || UINT64_MAX - usage.ram_usage>= (uint64_t)ram_delta, transaction_exception, "Ram usage delta would overflow UINT64_MAX"); - EOS_ASSERT(ram_delta > 0 || usage.ram_usage >= (uint64_t)(-ram_delta), transaction_exception, + EOS_ASSERT(ram_delta > 0 || usage.ram_usage >= (uint64_t)(-ram_delta), transaction_exception, "Ram usage delta would underflow UINT64_MAX"); if( limits.ram_bytes >= 0 && usage.ram_usage > limits.ram_bytes ) { diff --git a/libraries/chain/transaction_context.cpp b/libraries/chain/transaction_context.cpp index 5122241651a7c4a6575b5e5afe7f0e5417b2ca2a..1a8742a53fc29469a9809b310aff4cfe7e8c0347 100644 --- a/libraries/chain/transaction_context.cpp +++ b/libraries/chain/transaction_context.cpp @@ -13,10 +13,8 @@ namespace eosio { namespace chain { EOS_ASSERT( trx.max_kcpu_usage.value < UINT32_MAX / 1024UL, transaction_exception, "declared max_kcpu_usage overflows when expanded to max cpu usage" ); EOS_ASSERT( trx.max_net_usage_words.value < UINT32_MAX / 8UL, transaction_exception, "declared max_net_usage_words overflows when expanded to max net usage" ); - - control.record_transaction( trx_meta ); /// checks for dupes - control.validate_tapos( trx ); + control.validate_tapos( trx ); control.validate_referenced_accounts( trx ); control.validate_expiration( trx ); diff --git a/libraries/chain/wasm_interface.cpp b/libraries/chain/wasm_interface.cpp index 9c4d145fb184fcd8ec91937d34305524dc769030..2f6052772b38158a4945ca56f97843588e673316 100644 --- a/libraries/chain/wasm_interface.cpp +++ b/libraries/chain/wasm_interface.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -766,7 +767,7 @@ class permission_api : public context_aware_api { pub_keys.emplace_back(pub); } - return context.control.check_authorization( + return context.control.get_authorization_manager().check_authorization( account, permission, {pub_keys.begin(), pub_keys.end()}, false diff --git a/libraries/testing/include/eosio/testing/tester.hpp b/libraries/testing/include/eosio/testing/tester.hpp index 1dfbecdb03bf285cc26f9c39b494e1c64c57727d..37c148ead192941cf4d36fa27b26287e98f33301 100644 --- a/libraries/testing/include/eosio/testing/tester.hpp +++ b/libraries/testing/include/eosio/testing/tester.hpp @@ -86,6 +86,7 @@ namespace eosio { namespace testing { transaction_trace_ptr push_action( const account_name& code, const action_name& acttype, const account_name& actor, const variant_object& data, uint32_t expiration = DEFAULT_EXPIRATION_DELTA, uint32_t delay_sec = 0 ); transaction_trace_ptr push_action( const account_name& code, const action_name& acttype, const vector& actors, const variant_object& data, uint32_t expiration = DEFAULT_EXPIRATION_DELTA, uint32_t delay_sec = 0 ); + transaction_trace_ptr push_action( const account_name& code, const action_name& acttype, const vector& auths, const variant_object& data, uint32_t expiration = DEFAULT_EXPIRATION_DELTA, uint32_t delay_sec = 0 ); void set_tapos( signed_transaction& trx, uint32_t expiration = DEFAULT_EXPIRATION_DELTA ) const; diff --git a/libraries/testing/tester.cpp b/libraries/testing/tester.cpp index 8f4da826ccc3e51999b217358d1fce17a77da3cf..6c508d7bf8122a5e45f7e5b70dd36bf091241a52 100644 --- a/libraries/testing/tester.cpp +++ b/libraries/testing/tester.cpp @@ -200,25 +200,43 @@ namespace eosio { namespace testing { return success(); } - transaction_trace_ptr base_tester::push_action( const account_name& code, - const action_name& acttype, - const account_name& actor, - const variant_object& data, - uint32_t expiration, - uint32_t delay_sec) + const action_name& acttype, + const account_name& actor, + const variant_object& data, + uint32_t expiration, + uint32_t delay_sec + ) { try { - return push_action(code, acttype, vector{ actor }, data, expiration, delay_sec); + vector auths; + auths.push_back(permission_level{actor, config::active_name}); + return push_action(code, acttype, auths, data, expiration, delay_sec); } FC_CAPTURE_AND_RETHROW( (code)(acttype)(actor)(data)(expiration) ) } + transaction_trace_ptr base_tester::push_action( const account_name& code, + const action_name& acttype, + const vector& actors, + const variant_object& data, + uint32_t expiration, + uint32_t delay_sec + ) + + { try { + vector auths; + for (const auto& actor : actors) { + auths.push_back(permission_level{actor, config::active_name}); + } + return push_action(code, acttype, auths, data, expiration, delay_sec); + } FC_CAPTURE_AND_RETHROW( (code)(acttype)(actors)(data)(expiration) ) } transaction_trace_ptr base_tester::push_action( const account_name& code, - const action_name& acttype, - const vector& actors, - const variant_object& data, - uint32_t expiration, - uint32_t delay_sec) + const action_name& acttype, + const vector& auths, + const variant_object& data, + uint32_t expiration, + uint32_t delay_sec + ) { try { const auto& acnt = control->db().get(code); @@ -233,21 +251,18 @@ namespace eosio { namespace testing { action act; act.account = code; act.name = acttype; - for (const auto& actor : actors) { - act.authorization.push_back(permission_level{actor, config::active_name}); - } + act.authorization = auths; act.data = abis.variant_to_binary(action_type_name, data); signed_transaction trx; trx.actions.emplace_back(std::move(act)); set_transaction_headers(trx, expiration, delay_sec); - for (const auto& actor : actors) { - trx.sign(get_private_key(actor, "active"), chain_id_type()); + for (const auto& auth : auths) { + trx.sign(get_private_key(auth.actor, auth.permission.to_string()), chain_id_type()); } return push_transaction(trx); - } FC_CAPTURE_AND_RETHROW( (code)(acttype)(actors)(data)(expiration) ) } - + } FC_CAPTURE_AND_RETHROW( (code)(acttype)(auths)(data)(expiration) ) } transaction_trace_ptr base_tester::push_reqauth( account_name from, const vector& auths, const vector& keys ) { variant pretty_trx = fc::mutable_variant_object() diff --git a/plugins/chain_plugin/chain_plugin.cpp b/plugins/chain_plugin/chain_plugin.cpp index 3acc702965ef710ca7bd7282eef705a4bff69ae9..bfed18a24485309a4a1c4a62e638e55c572deb6f 100644 --- a/plugins/chain_plugin/chain_plugin.cpp +++ b/plugins/chain_plugin/chain_plugin.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include #include @@ -487,7 +487,7 @@ read_only::abi_bin_to_json_result read_only::abi_bin_to_json( const read_only::a read_only::get_required_keys_result read_only::get_required_keys( const get_required_keys_params& params )const { transaction pretty_input; from_variant(params.transaction, pretty_input); - auto required_keys_set = db.get_required_keys(pretty_input, params.available_keys); + auto required_keys_set = db.get_authorization_manager().get_required_keys(pretty_input, params.available_keys); get_required_keys_result result; result.required_keys = required_keys_set; return result; diff --git a/tests/chain_tests/auth_tests.cpp b/tests/chain_tests/auth_tests.cpp index 69450ce33e6039fe5d7270b136d6fafc62ab60a3..143842148ed97a7c41cea2f9cd1ffc12cbbff905 100644 --- a/tests/chain_tests/auth_tests.cpp +++ b/tests/chain_tests/auth_tests.cpp @@ -78,6 +78,7 @@ BOOST_AUTO_TEST_CASE(update_auths) { try { TESTER chain; chain.create_account("alice"); + chain.create_account("bob"); // Deleting active or owner should fail BOOST_CHECK_THROW(chain.delete_authority("alice", "active"), action_validate_exception); @@ -132,6 +133,11 @@ try { auto trading_priv_key = chain.get_private_key("alice", "trading"); auto trading_pub_key = trading_priv_key.get_public_key(); + // Bob attempts to create new spending auth for Alice + BOOST_CHECK_THROW( chain.set_authority( "alice", "spending", authority(spending_pub_key), "active", + { permission_level{"bob", "active"} }, { chain.get_private_key("bob", "active") } ), + transaction_exception ); + // Create new spending auth chain.set_authority("alice", "spending", authority(spending_pub_key), "active", { permission_level{"alice", "active"} }, { new_active_priv_key }); diff --git a/tests/chain_tests/delay_tests.cpp b/tests/chain_tests/delay_tests.cpp index bb31f062093f9a6e9fb87c2250a8dceda5c89ef6..b126ddbc9cccbdb9dc34215c783ddc6febab6193 100644 --- a/tests/chain_tests/delay_tests.cpp +++ b/tests/chain_tests/delay_tests.cpp @@ -1494,8 +1494,7 @@ BOOST_AUTO_TEST_CASE( canceldelay_test ) { try { // send canceldelay for first delayed transaction signed_transaction trx; trx.actions.emplace_back(vector{{N(tester), config::active_name}}, - chain::canceldelay{ids[0]}); - trx.actions.back().authorization.push_back({N(tester), config::active_name}); + chain::contracts::canceldelay{{N(tester), config::active_name}, ids[0]}); chain.set_transaction_headers(trx); trx.sign(chain.get_private_key(N(tester), "active"), chain_id_type()); @@ -1514,7 +1513,7 @@ BOOST_AUTO_TEST_CASE( canceldelay_test ) { try { liquid_balance = get_currency_balance(chain, N(tester2)); BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); - // first transfer will finally be performed + // update auth will finally be performed chain.produce_blocks(); liquid_balance = get_currency_balance(chain, N(tester)); @@ -1560,6 +1559,259 @@ BOOST_AUTO_TEST_CASE( canceldelay_test ) { try { BOOST_REQUIRE_EQUAL(asset::from_string("85.0000 CUR"), liquid_balance); liquid_balance = get_currency_balance(chain, N(tester2)); BOOST_REQUIRE_EQUAL(asset::from_string("15.0000 CUR"), liquid_balance); -} FC_LOG_AND_RETHROW() }/// schedule_test +} FC_LOG_AND_RETHROW() } + +// test canceldelay action under different permission levels +BOOST_AUTO_TEST_CASE( canceldelay_test2 ) { try { + TESTER chain; + + const auto& tester_account = N(tester); + std::vector ids; + chain.set_code(config::system_account_name, eosio_system_wast); + chain.set_abi(config::system_account_name, eosio_system_abi); + + chain.produce_blocks(); + chain.create_account(N(currency)); + chain.produce_blocks(); + + chain.set_code(N(currency), currency_wast); + chain.set_abi(N(currency), currency_abi); + + chain.produce_blocks(); + chain.create_account(N(tester)); + chain.create_account(N(tester2)); + chain.produce_blocks(); + + chain.push_action(config::system_account_name, contracts::updateauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("permission", "first") + ("parent", "active") + ("data", authority(chain.get_public_key(tester_account, "first"))) + ("delay", 5)); + chain.push_action(config::system_account_name, contracts::updateauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("permission", "second") + ("parent", "first") + ("data", authority(chain.get_public_key(tester_account, "second"))) + ("delay", 0)); + chain.push_action(config::system_account_name, contracts::linkauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("code", "currency") + ("type", "transfer") + ("requirement", "first")); + + chain.produce_blocks(); + chain.push_action(N(currency), N(create), N(currency), mutable_variant_object() + ("issuer", "currency" ) + ("maximum_supply", "9000000.0000 CUR" ) + ("can_freeze", 0) + ("can_recall", 0) + ("can_whitelist", 0) + ); + + chain.push_action(N(currency), name("issue"), N(currency), fc::mutable_variant_object() + ("to", "currency") + ("quantity", "1000000.0000 CUR") + ("memo", "for stuff") + ); + + auto trace = chain.push_action(N(currency), name("transfer"), N(currency), fc::mutable_variant_object() + ("from", "currency") + ("to", "tester") + ("quantity", "100.0000 CUR") + ("memo", "hi" ) + ); + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace.status); + BOOST_REQUIRE_EQUAL(0, trace.deferred_transaction_requests.size()); + + chain.produce_blocks(); + auto liquid_balance = get_currency_balance(chain, N(currency)); + BOOST_REQUIRE_EQUAL(asset::from_string("999900.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, N(tester)); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + + ilog("attempting first delayed transfer"); + + { + // this transaction will be delayed 10 blocks + trace = chain.push_action(N(currency), name("transfer"), vector{{N(tester), N(first)}}, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "1.0000 CUR") + ("memo", "hi" ), + 30, 5 + ); + auto trx_id = trace.id; + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace.status); + BOOST_REQUIRE_EQUAL(1, trace.deferred_transaction_requests.size()); + BOOST_REQUIRE_EQUAL(0, trace.action_traces.size()); + + const auto sender_id_to_cancel = trace.deferred_transaction_requests[0].get().sender_id; + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, N(tester)); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, N(tester2)); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // attempt canceldelay with wrong canceling_auth for delayed transfer of 1.0000 CUR + { + signed_transaction trx; + trx.actions.emplace_back(vector{{N(tester), config::active_name}}, + chain::contracts::canceldelay{{N(tester), config::active_name}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key(N(tester), "active"), chain_id_type()); + BOOST_REQUIRE_THROW( chain.push_transaction(trx), transaction_exception ); + } + + // attempt canceldelay with "second" permission for delayed transfer of 1.0000 CUR + { + signed_transaction trx; + trx.actions.emplace_back(vector{{N(tester), N(second)}}, + chain::contracts::canceldelay{{N(tester), N(first)}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key(N(tester), "second"), chain_id_type()); + BOOST_REQUIRE_THROW( chain.push_transaction(trx), tx_irrelevant_auth ); + } + + // canceldelay with "active" permission for delayed transfer of 1.0000 CUR + signed_transaction trx; + trx.actions.emplace_back(vector{{N(tester), config::active_name}}, + chain::contracts::canceldelay{{N(tester), N(first)}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key(N(tester), "active"), chain_id_type()); + trace = chain.push_transaction(trx); + + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace.status); + BOOST_REQUIRE_EQUAL(1, trace.deferred_transaction_requests.size()); + + const auto sender_id_canceled = trace.deferred_transaction_requests[0].get().sender_id; + BOOST_REQUIRE_EQUAL(std::string(uint128(sender_id_to_cancel)), std::string(uint128(sender_id_canceled))); + + chain.produce_blocks(10); + + liquid_balance = get_currency_balance(chain, N(tester)); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, N(tester2)); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + } + + ilog("reset minimum permission of transfer to second permission"); + + chain.push_action(config::system_account_name, contracts::linkauth::get_name(), tester_account, fc::mutable_variant_object() + ("account", "tester") + ("code", "currency") + ("type", "transfer") + ("requirement", "second"), + 30, 5 + ); + + chain.produce_blocks(10); + + + ilog("attempting second delayed transfer"); + { + // this transaction will be delayed 10 blocks + trace = chain.push_action(N(currency), name("transfer"), vector{{N(tester), N(second)}}, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "5.0000 CUR") + ("memo", "hi" ), + 30, 5 + ); + auto trx_id = trace.id; + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace.status); + BOOST_REQUIRE_EQUAL(1, trace.deferred_transaction_requests.size()); + BOOST_REQUIRE_EQUAL(0, trace.action_traces.size()); + + const auto sender_id_to_cancel = trace.deferred_transaction_requests[0].get().sender_id; + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, N(tester)); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, N(tester2)); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // canceldelay with "first" permission for delayed transfer of 5.0000 CUR + signed_transaction trx; + trx.actions.emplace_back(vector{{N(tester), N(first)}}, + chain::contracts::canceldelay{{N(tester), N(second)}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key(N(tester), "first"), chain_id_type()); + trace = chain.push_transaction(trx); + + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace.status); + BOOST_REQUIRE_EQUAL(1, trace.deferred_transaction_requests.size()); + + const auto sender_id_canceled = trace.deferred_transaction_requests[0].get().sender_id; + BOOST_REQUIRE_EQUAL(std::string(uint128(sender_id_to_cancel)), std::string(uint128(sender_id_canceled))); + + chain.produce_blocks(10); + + liquid_balance = get_currency_balance(chain, N(tester)); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, N(tester2)); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + } + + ilog("attempting third delayed transfer"); + + { + // this transaction will be delayed 10 blocks + trace = chain.push_action(N(currency), name("transfer"), vector{{N(tester), config::owner_name}}, fc::mutable_variant_object() + ("from", "tester") + ("to", "tester2") + ("quantity", "10.0000 CUR") + ("memo", "hi" ), + 30, 5 + ); + auto trx_id = trace.id; + BOOST_REQUIRE_EQUAL(transaction_receipt::delayed, trace.status); + BOOST_REQUIRE_EQUAL(1, trace.deferred_transaction_requests.size()); + BOOST_REQUIRE_EQUAL(0, trace.action_traces.size()); + + const auto sender_id_to_cancel = trace.deferred_transaction_requests[0].get().sender_id; + + chain.produce_blocks(); + + liquid_balance = get_currency_balance(chain, N(tester)); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, N(tester2)); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + + // attempt canceldelay with "active" permission for delayed transfer of 10.0000 CUR + { + signed_transaction trx; + trx.actions.emplace_back(vector{{N(tester), N(active)}}, + chain::contracts::canceldelay{{N(tester), config::owner_name}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key(N(tester), "active"), chain_id_type()); + BOOST_REQUIRE_THROW( chain.push_transaction(trx), tx_irrelevant_auth ); + } + + // canceldelay with "owner" permission for delayed transfer of 10.0000 CUR + signed_transaction trx; + trx.actions.emplace_back(vector{{N(tester), config::owner_name}}, + chain::contracts::canceldelay{{N(tester), config::owner_name}, trx_id}); + chain.set_transaction_headers(trx); + trx.sign(chain.get_private_key(N(tester), "owner"), chain_id_type()); + trace = chain.push_transaction(trx); + + BOOST_REQUIRE_EQUAL(transaction_receipt::executed, trace.status); + BOOST_REQUIRE_EQUAL(1, trace.deferred_transaction_requests.size()); + + const auto sender_id_canceled = trace.deferred_transaction_requests[0].get().sender_id; + BOOST_REQUIRE_EQUAL(std::string(uint128(sender_id_to_cancel)), std::string(uint128(sender_id_canceled))); + + chain.produce_blocks(10); + + liquid_balance = get_currency_balance(chain, N(tester)); + BOOST_REQUIRE_EQUAL(asset::from_string("100.0000 CUR"), liquid_balance); + liquid_balance = get_currency_balance(chain, N(tester2)); + BOOST_REQUIRE_EQUAL(asset::from_string("0.0000 CUR"), liquid_balance); + } +} FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END()