diff --git a/contracts/eosiolib/transaction.hpp b/contracts/eosiolib/transaction.hpp index 855452e9d4c58f70ab8416b095f669b2361734d6..6150c408cac97caf2c0343d30d6ee19261ca1280 100644 --- a/contracts/eosiolib/transaction.hpp +++ b/contracts/eosiolib/transaction.hpp @@ -28,7 +28,7 @@ namespace eosio { :expiration(exp),region(r) {} - void send(uint64_t sender_id, account_name payer = account_name(0), time delay_until = 0) const { + void send(uint64_t sender_id, account_name payer = account_name(0), time delay_until = now()) const { auto serialize = pack(*this); send_deferred(sender_id, payer, delay_until, serialize.data(), serialize.size()); } diff --git a/contracts/test_api/test_action.cpp b/contracts/test_api/test_action.cpp index a2f76d2b132acdb1182aa82837c7230039fe20e9..a01c53fa2bf245a93062ccb56f8f19075ad83d88 100644 --- a/contracts/test_api/test_action.cpp +++ b/contracts/test_api/test_action.cpp @@ -34,7 +34,7 @@ void test_action::read_action_normal() { eosio_assert(total == sizeof(dummy_action), "read_action(sizeof(dummy_action))" ); dummy_action *dummy13 = reinterpret_cast(buffer); - + eosio_assert(dummy13->a == DUMMY_ACTION_DEFAULT_A, "dummy13->a == DUMMY_ACTION_DEFAULT_A"); eosio_assert(dummy13->b == DUMMY_ACTION_DEFAULT_B, "dummy13->b == DUMMY_ACTION_DEFAULT_B"); eosio_assert(dummy13->c == DUMMY_ACTION_DEFAULT_C, "dummy13->c == DUMMY_ACTION_DEFAULT_C"); @@ -175,7 +175,7 @@ void test_action::test_current_receiver(uint64_t receiver, uint64_t code, uint64 (void)code;(void)action; account_name cur_rec; read_action_data(&cur_rec, sizeof(account_name)); - + eosio_assert( receiver == cur_rec, "the current receiver does not match" ); } diff --git a/contracts/test_api/test_api.cpp b/contracts/test_api/test_api.cpp index ec91fb4e36e0fbf40ead97a491a63d492c0557ee..780667fbde9a6978354e8e94ad290fabe2596b11 100644 --- a/contracts/test_api/test_api.cpp +++ b/contracts/test_api/test_api.cpp @@ -68,7 +68,7 @@ extern "C" { if ( action == N(dummy_action) ) { test_action::test_dummy_action(); return; - } + } //test_print WASM_TEST_HANDLER(test_print, test_prints); WASM_TEST_HANDLER(test_print, test_prints_l); @@ -122,6 +122,7 @@ extern "C" { WASM_TEST_HANDLER(test_transaction, send_transaction_empty); WASM_TEST_HANDLER(test_transaction, send_transaction_large); WASM_TEST_HANDLER(test_transaction, send_action_sender); + WASM_TEST_HANDLER(test_transaction, send_transaction_expiring_late); WASM_TEST_HANDLER(test_transaction, deferred_print); WASM_TEST_HANDLER(test_transaction, send_deferred_transaction); WASM_TEST_HANDLER(test_transaction, cancel_deferred_transaction); diff --git a/contracts/test_api/test_api.hpp b/contracts/test_api/test_api.hpp index 0d77f83931eb0f1a84ade670f3c703b23a8750dd..01d683ac04f9c42e142db992755f49450fb95e77 100644 --- a/contracts/test_api/test_api.hpp +++ b/contracts/test_api/test_api.hpp @@ -200,6 +200,7 @@ struct test_transaction { static void send_transaction_empty(); static void send_transaction_max(); static void send_transaction_large(); + static void send_transaction_expiring_late(); static void send_action_sender(); static void deferred_print(); static void send_deferred_transaction(); diff --git a/contracts/test_api/test_transaction.cpp b/contracts/test_api/test_transaction.cpp index 4eef84c7e2242f22a16e68e284ef1a8d7d3ec495..eaf10e3c8935055382b3250b38efcaee303f0388 100644 --- a/contracts/test_api/test_transaction.cpp +++ b/contracts/test_api/test_transaction.cpp @@ -23,11 +23,11 @@ struct test_action_action { template friend DataStream& operator << ( DataStream& ds, const test_action_action& a ) { - for ( auto c : a.data ) + for ( auto c : a.data ) ds << c; return ds; } - /* + /* template friend DataStream& operator >> ( DataStream& ds, test_action_action& a ) { return ds; @@ -56,7 +56,7 @@ struct test_dummy_action { ds << da.c; return ds; } - + template friend DataStream& operator >> ( DataStream& ds, test_dummy_action& da ) { ds >> da.a; @@ -95,7 +95,7 @@ void test_transaction::send_action_large() { using namespace eosio; char large_message[8 * 1024]; test_action_action test_action; - copy_data(large_message, 8*1024, test_action.data); + copy_data(large_message, 8*1024, test_action.data); action act(vector{{N(testapi), N(active)}}, test_action); act.send(); eosio_assert(false, "send_message_large() should've thrown an error"); @@ -110,9 +110,9 @@ void test_transaction::send_action_recurse() { read_action_data(buffer, 1024); test_action_action test_action; - copy_data(buffer, 1024, test_action.data); + copy_data(buffer, 1024, test_action.data); action act(vector{{N(testapi), N(active)}}, test_action); - + act.send(); } @@ -166,8 +166,8 @@ void test_transaction::send_transaction() { dummy_action payload = {DUMMY_ACTION_DEFAULT_A, DUMMY_ACTION_DEFAULT_B, DUMMY_ACTION_DEFAULT_C}; test_action_action test_action; - copy_data((char*)&payload, sizeof(dummy_action), test_action.data); - + copy_data((char*)&payload, sizeof(dummy_action), test_action.data); + auto trx = transaction(); trx.actions.emplace_back(vector{{N(testapi), N(active)}}, test_action); trx.send(0); @@ -202,7 +202,7 @@ void test_transaction::send_transaction_large() { for (int i = 0; i < 32; i ++) { char large_message[1024]; test_action_action test_action; - copy_data(large_message, 1024, test_action.data); + copy_data(large_message, 1024, test_action.data); trx.actions.emplace_back(vector{{N(testapi), N(active)}}, test_action); } @@ -211,6 +211,20 @@ void test_transaction::send_transaction_large() { eosio_assert(false, "send_transaction_large() should've thrown an error"); } +void test_transaction::send_transaction_expiring_late() { + using namespace eosio; + account_name cur_send; + read_action_data( &cur_send, sizeof(account_name) ); + test_action_action test_action; + copy_data((char*)&cur_send, sizeof(account_name), test_action.data); + + auto trx = transaction(now() + 60*60*24*365); + trx.actions.emplace_back(vector{{N(testapi), N(active)}}, test_action); + trx.send(0); + + eosio_assert(false, "send_transaction_expiring_late() should've thrown an error"); +} + /** * deferred transaction */ diff --git a/libraries/chain/apply_context.cpp b/libraries/chain/apply_context.cpp index d253c75eb0235d1bdc53b45556fc7d06c308880d..0e8b9095c7dc76fe141d8bec6afacc8a4e6d05d9 100644 --- a/libraries/chain/apply_context.cpp +++ b/libraries/chain/apply_context.cpp @@ -231,8 +231,8 @@ void apply_context::execute_inline( action&& a ) { if( a.account != receiver ) { const auto delay = controller.check_authorization({a}, vector(), flat_set(), false, {receiver}); FC_ASSERT( trx_meta.published + delay <= controller.head_block_time(), - "inline action uses a permission that imposes a delay that is not met, add an action of mindelay with delay of atleast ${delay}", - ("delay", delay.sec_since_epoch()) ); + "inline action uses a permission that imposes a delay that is not met, add an action of mindelay with delay of at least ${delay} seconds", + ("delay", delay.to_seconds()) ); } } _inline_actions.emplace_back( move(a) ); @@ -245,11 +245,20 @@ void apply_context::execute_context_free_inline( action&& a ) { void apply_context::execute_deferred( deferred_transaction&& trx ) { try { - FC_ASSERT( trx.expiration > (controller.head_block_time() + fc::milliseconds(2*config::block_interval_ms)), - "transaction is expired when created" ); + trx.set_reference_block(controller.head_block_id()); // No TaPoS check necessary + trx.sender = receiver; + controller.validate_transaction_without_state(trx); + // transaction_api::send_deferred guarantees that trx.execute_after is at least head block time, so no need to check expiration. + // Any other called of this function needs to similarly meet that precondition. + EOS_ASSERT( trx.execute_after < trx.expiration, + transaction_exception, + "Transaction expires at ${trx.expiration} which is before the contract-imposed first allowed time to execute at ${trx.execute_after}", + ("trx.expiration",trx.expiration)("trx.execute_after",trx.execute_after) ); - FC_ASSERT( trx.execute_after < trx.expiration, "transaction expires before it can execute" ); - FC_ASSERT( !trx.actions.empty(), "transaction must have at least one action"); + controller.validate_expiration_not_too_far(trx, trx.execute_after); + controller.validate_referenced_accounts(trx); + + controller.validate_uniqueness(trx); // TODO: Move this out of here when we have concurrent shards to somewhere we can check for conflicts between shards. if (trx.payer != receiver) { require_authorization(trx.payer); @@ -258,6 +267,8 @@ void apply_context::execute_deferred( deferred_transaction&& trx ) { const auto& gpo = controller.get_global_properties(); FC_ASSERT( results.deferred_transactions_count < gpo.configuration.max_generated_transaction_count ); + fc::microseconds delay; + // privileged accounts can do anything, no need to check auth if( !privileged ) { @@ -271,15 +282,21 @@ void apply_context::execute_deferred( deferred_transaction&& trx ) { } } if( check_auth ) { - const auto delay = controller.check_authorization(trx.actions, vector(), flat_set(), false, {receiver}); + delay = controller.check_authorization(trx.actions, vector(), 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, add an action of mindelay with delay of atleast ${delay}", - ("delay", delay.sec_since_epoch()) ); + "deferred transaction uses a permission that imposes a delay that is not met, add an action of mindelay with delay of at least ${delay} seconds", + ("delay", delay.to_seconds()) ); } } - trx.sender = receiver; // "Attempting to send from another account" - trx.set_reference_block(controller.head_block_id()); + auto now = controller.head_block_time(); + if( delay.count() ) { + trx.execute_after = std::max(trx.execute_after, time_point_sec(now + delay + fc::microseconds(999'999)) /* rounds up nearest second */ ); + EOS_ASSERT( trx.execute_after < trx.expiration, + 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) ); + } results.deferred_transaction_requests.push_back(move(trx)); results.deferred_transactions_count++; diff --git a/libraries/chain/chain_controller.cpp b/libraries/chain/chain_controller.cpp index 6cd2e6004577eb0318a4882631c87aae2e7e8067..72d6c54b38e990908b00bf8c48f5cfd5e9326b78 100644 --- a/libraries/chain/chain_controller.cpp +++ b/libraries/chain/chain_controller.cpp @@ -289,61 +289,23 @@ transaction_trace chain_controller::_push_transaction(const packed_transaction& transaction_metadata mtrx( packed_trx, get_chain_id(), head_block_time()); //idump((transaction_header(mtrx.trx()))); - const auto delay = check_transaction_authorization(mtrx.trx(), packed_trx.signatures, packed_trx.context_free_data); - auto setup_us = fc::time_point::now() - start; + const transaction& trx = mtrx.trx(); + validate_transaction_with_minimal_state( packed_trx, &trx ); + validate_referenced_accounts(trx); + validate_uniqueness(trx); + auto delay = check_transaction_authorization(trx, packed_trx.signatures, packed_trx.context_free_data); + validate_expiration_not_too_far(trx, head_block_time() + delay ); + mtrx.delay = delay; - // enforce that the header is accurate as a commitment to net_usage - uint32_t cfa_sig_net_usage = (uint32_t)(packed_trx.context_free_data.size() + fc::raw::pack_size(packed_trx.signatures)); - uint32_t net_usage_commitment = mtrx.trx().net_usage_words.value * 8U; - uint32_t packed_size = (uint32_t)packed_trx.data.size(); - uint32_t net_usage = cfa_sig_net_usage + packed_size; - EOS_ASSERT(net_usage <= net_usage_commitment, - tx_resource_exhausted, - "Packed Transaction and associated data does not fit into the space committed to by the transaction's header! [usage=${usage},commitment=${commit}]", - ("usage", net_usage)("commit", net_usage_commitment)); + auto setup_us = fc::time_point::now() - start; transaction_trace result(mtrx.id); - if (!delay.sec_since_epoch()) { - result = _push_transaction(std::move(mtrx)); + if( delay.count() == 0 ) { + result = _push_transaction( std::move(mtrx) ); } else { - auto delayed_transaction_processing = [&](transaction_metadata& meta) { - result.status = transaction_trace::delayed; - const auto trx = mtrx.trx(); - FC_ASSERT( !trx.actions.empty(), "transaction must have at least one action"); - - FC_ASSERT( trx.expiration > (head_block_time() + fc::milliseconds(2*config::block_interval_ms)), - "transaction is expired when created" ); - - // add in the system account authorization - action for_deferred = trx.actions[0]; - bool found = false; - for (const auto& auth : for_deferred.authorization) { - if (auth.actor == config::system_account_name && - auth.permission == config::active_name) { - found = true; - break; - } - } - if (!found) - for_deferred.authorization.push_back(permission_level{config::system_account_name, config::active_name}); - - apply_context context(*this, _db, for_deferred, mtrx); - - time_point_sec execute_after = head_block_time(); - execute_after += time_point_sec(delay); - //TODO: !!! WHO GETS BILLED TO STORE THE DELAYED TX? - deferred_transaction dtrx(context.get_next_sender_id(), config::system_account_name, config::system_account_name, execute_after, trx); - FC_ASSERT( dtrx.execute_after < dtrx.expiration, "transaction expires before it can execute" ); - - result.deferred_transaction_requests.push_back(std::move(dtrx)); - - _create_generated_transaction(result.deferred_transaction_requests[0].get()); - - return result; - }; - - result = wrap_transaction_processing( move(mtrx), delayed_transaction_processing); + result = wrap_transaction_processing( std::move(mtrx), + [this](transaction_metadata& meta) { return delayed_transaction_processing(meta); } ); } // notify anyone listening to pending transactions @@ -356,32 +318,18 @@ transaction_trace chain_controller::_push_transaction(const packed_transaction& } FC_CAPTURE_AND_RETHROW( (transaction_header(packed_trx.get_transaction())) ) } -static void record_locks_for_data_access(transaction_trace& trace, flat_set& read_locks, flat_set& write_locks ) { - for (const auto& at: trace.action_traces) { - for (const auto& access: at.data_access) { - if (access.type == data_access_info::read) { - trace.read_locks.emplace(shard_lock{access.code, access.scope}); - } else { - trace.write_locks.emplace(shard_lock{access.code, access.scope}); - } - } - } - - // remove read locks for write locks taken by other actions - std::for_each(trace.write_locks.begin(), trace.write_locks.end(), [&]( const shard_lock& l){ - trace.read_locks.erase(l); read_locks.erase(l); - }); - - read_locks.insert(trace.read_locks.begin(), trace.read_locks.end()); - write_locks.insert(trace.write_locks.begin(), trace.write_locks.end()); -} - transaction_trace chain_controller::_push_transaction( transaction_metadata&& data ) { try { auto process_apply_transaction = [this](transaction_metadata& meta) { auto cyclenum = _pending_block->regions.back().cycles_summary.size() - 1; //wdump((transaction_header(meta.trx()))); + const auto& trx = meta.trx(); + + // Validate uniqueness and expiration again + validate_uniqueness(trx); + validate_not_expired(trx); + /// TODO: move _pending_cycle into db so that it can be undone if transation fails, for now we will apply /// the transaction first so that there is nothing to undo... this only works because things are currently /// single threaded @@ -395,12 +343,83 @@ transaction_trace chain_controller::_push_transaction( transaction_metadata&& da return wrap_transaction_processing( move(data), process_apply_transaction ); } FC_CAPTURE_AND_RETHROW( ) } +transaction_trace chain_controller::delayed_transaction_processing( const transaction_metadata& mtrx ) +{ try { + transaction_trace result(mtrx.id); + result.status = transaction_trace::delayed; + + const auto& trx = mtrx.trx(); + + // add in the system account authorization + action for_deferred = trx.actions[0]; + bool found = false; + for (const auto& auth : for_deferred.authorization) { + if (auth.actor == config::system_account_name && + auth.permission == config::active_name) { + found = true; + break; + } + } + if (!found) + for_deferred.authorization.push_back(permission_level{config::system_account_name, config::active_name}); + + apply_context context(*this, _db, for_deferred, mtrx); // TODO: Better solution for getting next sender_id needed. + + time_point_sec execute_after = head_block_time(); + execute_after += mtrx.delay; + //TODO: !!! WHO GETS BILLED TO STORE THE DELAYED TX? + deferred_transaction dtrx(context.get_next_sender_id(), config::system_account_name, config::system_account_name, execute_after, trx); + FC_ASSERT( dtrx.execute_after < dtrx.expiration, "transaction expires before it can execute" ); + + result.deferred_transaction_requests.push_back(std::move(dtrx)); + + _create_generated_transaction(result.deferred_transaction_requests[0].get()); + + return result; + +} FC_CAPTURE_AND_RETHROW( ) } + +static void record_locks_for_data_access(transaction_trace& trace, flat_set& read_locks, flat_set& write_locks ) { + // Precondition: read_locks and write_locks do not intersect. + + for (const auto& at: trace.action_traces) { + for (const auto& access: at.data_access) { + if (access.type == data_access_info::read) { + trace.read_locks.emplace(shard_lock{access.code, access.scope}); + } else { + trace.write_locks.emplace(shard_lock{access.code, access.scope}); + } + } + } + + // Step RR: Remove from trace.read_locks and from read_locks only the read locks necessary to ensure they each do not intersect with trace.write_locks. + std::for_each(trace.write_locks.begin(), trace.write_locks.end(), [&]( const shard_lock& l){ + trace.read_locks.erase(l); read_locks.erase(l); // for step RR + // write_locks.insert(l); // Step AW could instead be done here, but it would add unnecessary work to the lookups in step AR. + }); + + // At this point, the trace.read_locks and trace.write_locks are good. + + // Step AR: Add into read_locks the subset of trace.read_locks that does not intersect with write_locks (must occur after step RR). + // (Works regardless of whether step AW is done before or after this step.) + std::for_each(trace.read_locks.begin(), trace.read_locks.end(), [&]( const shard_lock& l){ + if( write_locks.find(l) == write_locks.end() ) + read_locks.insert(l); + }); + + + // Step AW: Add trace.write_locks into write_locks. + write_locks.insert(trace.write_locks.begin(), trace.write_locks.end()); + + // Postcondition: read_locks and write_locks do not intersect + // Postcondition: trace.read_locks and trace.write_locks do not intersect +} block_header chain_controller::head_block_header() const { auto b = _fork_db.fetch_block(head_block_id()); if( b ) return b->data; - + if (auto head_block = fetch_block_by_id(head_block_id())) return *head_block; return block_header(); @@ -513,6 +532,7 @@ void chain_controller::_apply_cycle_trace( const cycle_trace& res ) auto &generated_transaction_idx = _db.get_mutable_index(); const auto &generated_index = generated_transaction_idx.indices().get(); + // TODO: Check for conflicts in deferred_transaction_requests between shards for (const auto&st: res.shard_traces) { for (const auto &tr: st.transaction_traces) { for (const auto &req: tr.deferred_transaction_requests) { @@ -554,9 +574,8 @@ void chain_controller::_apply_cycle_trace( const cycle_trace& res ) * After applying all transactions successfully we can update * the current block time, block number, producer stats, etc */ -void chain_controller::_finalize_block( const block_trace& trace ) { try { +void chain_controller::_finalize_block( const block_trace& trace, const producer_object& signing_producer ) { try { const auto& b = trace.block; - const producer_object& signing_producer = validate_block_header(_skip_flags, b); update_global_properties( b ); update_global_dynamic_data( b ); @@ -597,6 +616,7 @@ signed_block chain_controller::_generate_block( block_timestamp_type when, { try { try { + FC_ASSERT( head_block_time() < (fc::time_point)when, "block must be generated at a timestamp after the head block time" ); uint32_t skip = _skip_flags; uint32_t slot_num = get_slot_at_time( when ); FC_ASSERT( slot_num > 0 ); @@ -632,7 +652,7 @@ signed_block chain_controller::_generate_block( block_timestamp_type when, if( !(skip & skip_producer_signature) ) _pending_block->sign( block_signing_key ); - _finalize_block( *_pending_block_trace ); + _finalize_block( *_pending_block_trace, producer_obj ); _pending_block_session->push(); @@ -719,13 +739,6 @@ void chain_controller::__apply_block(const signed_block& next_block) uint32_t skip = _skip_flags; - /* - FC_ASSERT((skip & skip_merkle_check) - || next_block.transaction_merkle_root == next_block.calculate_merkle_root(), - "", ("next_block.transaction_merkle_root", next_block.transaction_merkle_root) - ("calc",next_block.calculate_merkle_root())("next_block",next_block)("id",next_block.id())); - */ - const producer_object& signing_producer = validate_block_header(skip, next_block); /// regions must be listed in order @@ -743,7 +756,7 @@ void chain_controller::__apply_block(const signed_block& next_block) } input_metas.reserve(next_block.input_transactions.size() + next_block_trace.implicit_transactions.size()); - + for ( const auto& t : next_block_trace.implicit_transactions ) { input_metas.emplace_back(packed_transaction(t), get_chain_id(), head_block_time(), true /*implicit*/); } @@ -751,6 +764,9 @@ void chain_controller::__apply_block(const signed_block& next_block) map trx_index; for( const auto& t : next_block.input_transactions ) { input_metas.emplace_back(t, chain_id_type(), next_block.timestamp); + validate_transaction_with_minimal_state( t, &input_metas.back().trx() ); + if( should_check_signatures() ) + input_metas.back().signing_keys = input_metas.back().trx().get_signature_keys( t.signatures, chain_id_type(), t.context_free_data, false ); trx_index[input_metas.back().id] = input_metas.size() - 1; } @@ -778,7 +794,7 @@ void chain_controller::__apply_block(const signed_block& next_block) EOS_ASSERT(!shard.empty(), tx_empty_shard,"region[${r_index}] cycle[${c_index}] shard[${s_index}] is empty", ("r_index",region_index)("c_index",cycle_index)("s_index",shard_index)); - // Validate that the shards scopes are correct and available + // Validate that the shards locks are unique and sorted validate_shard_locks(shard.read_locks, "read"); validate_shard_locks(shard.write_locks, "write"); @@ -808,18 +824,31 @@ void chain_controller::__apply_block(const signed_block& next_block) auto make_metadata = [&]() -> transaction_metadata* { auto itr = trx_index.find(receipt.id); if( itr != trx_index.end() ) { + auto& trx_meta = input_metas.at(itr->second); + const auto& trx = trx_meta.trx(); + validate_referenced_accounts(trx); + validate_uniqueness(trx); + FC_ASSERT( !should_check_signatures() || trx_meta.signing_keys, + "signing_keys missing from transaction_metadata of an input transaction" ); + auto delay = check_authorization( trx.actions, trx.context_free_actions, + should_check_signatures() ? *trx_meta.signing_keys : flat_set() ); + validate_expiration_not_too_far(trx, head_block_time() + delay ); + + trx_meta.delay = delay; return &input_metas.at(itr->second); } else { const auto* gtrx = _db.find(receipt.id); if (gtrx != nullptr) { - ilog( "defer" ); + //ilog( "defer" ); auto trx = fc::raw::unpack(gtrx->packed_trx.data(), gtrx->packed_trx.size()); - FC_ASSERT( trx.execute_after <= head_block_time() , "deffered transaction executed prematurely" ); + FC_ASSERT( trx.execute_after <= head_block_time() , "deferred transaction executed prematurely" ); + validate_not_expired( trx ); + validate_uniqueness( trx ); _temp.emplace(trx, gtrx->published, trx.sender, trx.sender_id, gtrx->packed_trx.data(), gtrx->packed_trx.size() ); _destroy_generated_transaction(*gtrx); return &*_temp; } else { - ilog( "implicit" ); + //ilog( "implicit" ); for ( size_t i=0; i < next_block_trace.implicit_transactions.size(); i++ ) { if ( input_metas[i].id == receipt.id ) return &input_metas[i]; @@ -830,6 +859,7 @@ void chain_controller::__apply_block(const signed_block& next_block) }; auto *mtrx = make_metadata(); + mtrx->region_id = r.region; mtrx->cycle_index = cycle_index; mtrx->shard_index = shard_index; @@ -837,17 +867,16 @@ void chain_controller::__apply_block(const signed_block& next_block) mtrx->allowed_write_locks.emplace(&shard.write_locks); mtrx->processing_deadline = processing_deadline; - s_trace.transaction_traces.emplace_back(_apply_transaction(*mtrx)); - record_locks_for_data_access(s_trace.transaction_traces.back(), used_read_locks, used_write_locks); - - EOS_ASSERT(receipt.status == s_trace.transaction_traces.back().status, tx_receipt_inconsistent_status, - "Received status of transaction from block (${rstatus}) does not match the applied transaction's status (${astatus})", - ("rstatus",receipt.status)("astatus",s_trace.transaction_traces.back().status)); + if( mtrx->delay.count() == 0 ) { + s_trace.transaction_traces.emplace_back(_apply_transaction(*mtrx)); + record_locks_for_data_access(s_trace.transaction_traces.back(), used_read_locks, used_write_locks); + } else { + s_trace.transaction_traces.emplace_back(delayed_transaction_processing(*mtrx)); + } + EOS_ASSERT( receipt.status == s_trace.transaction_traces.back().status, tx_receipt_inconsistent_status, + "Received status of transaction from block (${rstatus}) does not match the applied transaction's status (${astatus})", + ("rstatus",receipt.status)("astatus",s_trace.transaction_traces.back().status) ); - // validate_referenced_accounts(trx); - // Check authorization, and allow irrelevant signatures. - // If the block producer let it slide, we'll roll with it. - // check_transaction_authorization(trx, true); } /// for each transaction id EOS_ASSERT( boost::equal( used_read_locks, shard.read_locks ), @@ -869,14 +898,14 @@ void chain_controller::__apply_block(const signed_block& next_block) FC_ASSERT(next_block.action_mroot == next_block_trace.calculate_action_merkle_root()); FC_ASSERT( transaction_metadata::calculate_transaction_merkle_root(input_metas) == next_block.transaction_mroot, "merkle root does not match" ); - _finalize_block( next_block_trace ); + _finalize_block( next_block_trace, signing_producer ); } FC_CAPTURE_AND_RETHROW( (next_block.block_num()) ) } flat_set chain_controller::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; }, - [](const permission_level& ) {}, + noop_permission_visitor(), get_global_properties().configuration.max_authority_depth, candidate_keys); @@ -896,25 +925,48 @@ flat_set chain_controller::get_required_keys(const transaction& class permission_visitor { public: - permission_visitor(const chain_controller& controller) : _chain_controller(controller) {} + permission_visitor(const chain_controller& controller) + : _chain_controller(controller) { + _max_delay_stack.emplace_back(); + } void operator()(const permission_level& perm_level) { const auto obj = _chain_controller.get_permission(perm_level); - if (_max_delay < obj.delay) - _max_delay = obj.delay; + if( _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(); } - const time_point& get_max_delay() const { return _max_delay; } private: const chain_controller& _chain_controller; - time_point _max_delay; + vector _max_delay_stack; }; -time_point chain_controller::check_authorization( const vector& actions, - const vector& context_free_actions, - const flat_set& provided_keys, - bool allow_unused_signatures, - flat_set provided_accounts )const +fc::microseconds chain_controller::check_authorization( const vector& actions, + const vector& context_free_actions, + const flat_set& provided_keys, + bool allow_unused_signatures, + flat_set provided_accounts )const { auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; }, @@ -922,72 +974,67 @@ time_point chain_controller::check_authorization( const vector& actions, get_global_properties().configuration.max_authority_depth, provided_keys, provided_accounts ); - time_point max_delay; + fc::microseconds max_delay; for( const auto& act : actions ) { for( const auto& declared_auth : act.authorization ) { // 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.account, act.name); - bool min_permission_required = true; - if (!min_permission_name) { + if( !min_permission_name ) { // for updateauth actions, need to determine the permission that is changing - if (act.account == config::system_account_name && act.name == contracts::updateauth::get_name()) { + if( act.account == config::system_account_name && act.name == contracts::updateauth::get_name() ) { auto update = act.data_as(); const auto permission_to_change = _db.find(boost::make_tuple(update.account, update.permission)); - if (permission_to_change != nullptr) { - // only determining delay - min_permission_required = false; + if( permission_to_change != nullptr ) { + // Only changes to permissions need to possibly be delayed. New permissions can be added immediately. min_permission_name = update.permission; } } } - if (min_permission_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(); - const optional delay = get_permission(declared_auth).satisfies(min_permission, index); - EOS_ASSERT(!min_permission_required || delay.valid(), - tx_irrelevant_auth, - "action declares irrelevant authority '${auth}'; minimum authority is ${min}", - ("auth", declared_auth)("min", min_permission.name)); - if (max_delay < *delay) - max_delay = *delay; - } + const auto& index = _db.get_index().indices(); + const optional delay = get_permission(declared_auth).satisfies(min_permission, index); + EOS_ASSERT( delay.valid(), + tx_irrelevant_auth, + "action declares irrelevant authority '${auth}'; minimum authority is ${min}", + ("auth", declared_auth)("min", min_permission.name) ); + if( max_delay < *delay ) + max_delay = *delay; } - if (act.account == config::system_account_name) { + if( act.account == config::system_account_name ) { // for link changes, we need to also determine the delay associated with an existing link that is being // moved or removed - if (act.name == contracts::linkauth::get_name()) { + if( act.name == contracts::linkauth::get_name() ) { auto link = act.data_as(); - if (declared_auth.actor == link.account) { + if( declared_auth.actor == link.account ) { const auto linked_permission_name = lookup_linked_permission(link.account, link.code, link.type); - if( linked_permission_name.valid() ) { + if( linked_permission_name.valid() && *linked_permission_name != config::eosio_any_name ) { const auto& linked_permission = _db.get(boost::make_tuple(link.account, *linked_permission_name)); const auto& index = _db.get_index().indices(); - const optional delay = get_permission(declared_auth).satisfies(linked_permission, index); - if (delay.valid() && max_delay < *delay) + const optional delay = get_permission(declared_auth).satisfies(linked_permission, index); + if( delay.valid() && max_delay < *delay ) max_delay = *delay; } // else it is only a new link, so don't need to delay } - } else if (act.name == contracts::unlinkauth::get_name()) { + } else if( act.name == contracts::unlinkauth::get_name() ) { auto unlink = act.data_as(); - if (declared_auth.actor == unlink.account) { + if( declared_auth.actor == unlink.account ) { const auto unlinked_permission_name = lookup_linked_permission(unlink.account, unlink.code, unlink.type); - if (unlinked_permission_name.valid()) { + if( unlinked_permission_name.valid() && *unlinked_permission_name != config::eosio_any_name ) { const auto& unlinked_permission = _db.get(boost::make_tuple(unlink.account, *unlinked_permission_name)); const auto& index = _db.get_index().indices(); - const optional delay = get_permission(declared_auth).satisfies(unlinked_permission, index); - if (delay.valid() && max_delay < *delay) + const optional delay = get_permission(declared_auth).satisfies(unlinked_permission, index); + if( delay.valid() && max_delay < *delay ) max_delay = *delay; } } } } - if ((_skip_flags & skip_transaction_signatures) == false) { + if( should_check_signatures() ) { EOS_ASSERT(checker.satisfied(declared_auth), tx_missing_sigs, "transaction declares authority '${auth}', but does not have signatures for it.", ("auth", declared_auth)); @@ -998,19 +1045,20 @@ time_point chain_controller::check_authorization( const vector& actions, for( const auto& act : context_free_actions ) { if (act.account == config::system_account_name && act.name == contracts::mindelay::get_name()) { const auto mindelay = act.data_as(); - const time_point delay = time_point_sec{mindelay.delay.convert_to()}; - if (max_delay < delay) + auto delay = fc::seconds(mindelay.delay); + if( max_delay < delay ) max_delay = delay; } } - if (!allow_unused_signatures && (_skip_flags & skip_transaction_signatures) == false) - EOS_ASSERT(checker.all_keys_used(), tx_irrelevant_sig, - "transaction bears irrelevant signatures from these keys: ${keys}", - ("keys", checker.unused_keys())); + 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(); - if (max_delay < checker_max_delay) + if( max_delay < checker_max_delay ) max_delay = checker_max_delay; return max_delay; @@ -1021,13 +1069,13 @@ bool chain_controller::check_authorization( account_name account, permission_nam bool allow_unused_signatures)const { auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; }, - [](const permission_level& ) {}, - get_global_properties().configuration.max_authority_depth, - provided_keys); + noop_permission_visitor(), + get_global_properties().configuration.max_authority_depth, + provided_keys); auto satisfied = checker.satisfied({account, permission}); - if (satisfied && !allow_unused_signatures) { + if( satisfied && !allow_unused_signatures ) { EOS_ASSERT(checker.all_keys_used(), tx_irrelevant_sig, "irrelevant signatures from these keys: ${keys}", ("keys", checker.unused_keys())); @@ -1036,12 +1084,18 @@ bool chain_controller::check_authorization( account_name account, permission_nam return satisfied; } -time_point chain_controller::check_transaction_authorization(const transaction& trx, - const vector& signatures, - const vector& cfd, - bool allow_unused_signatures)const +fc::microseconds chain_controller::check_transaction_authorization(const transaction& trx, + const vector& signatures, + const vector& cfd, + bool allow_unused_signatures)const { - return check_authorization( trx.actions, trx.context_free_actions, trx.get_signature_keys( signatures, chain_id_type{}, cfd ), allow_unused_signatures ); + if( should_check_signatures() ) { + return check_authorization( trx.actions, trx.context_free_actions, + trx.get_signature_keys( signatures, chain_id_type{}, cfd, allow_unused_signatures ), + allow_unused_signatures ); + } else { + return check_authorization( trx.actions, trx.context_free_actions, flat_set(), true ); + } } optional chain_controller::lookup_minimum_permission(account_name authorizer_account, @@ -1050,16 +1104,16 @@ optional chain_controller::lookup_minimum_permission(account_na #warning TODO: this comment sounds like it is expecting a check ("may") somewhere else, but I have not found anything else // 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 == contracts::updateauth::get_name()) { + if( scope == config::system_account_name && act_name == contracts::updateauth::get_name() ) { return optional(); } try { optional linked_permission = lookup_linked_permission(authorizer_account, scope, act_name); - if (!linked_permission ) + if( !linked_permission ) return config::active_name; - if( *linked_permission == N(eosio.any) ) + if( *linked_permission == config::eosio_any_name ) return optional(); return linked_permission; @@ -1082,8 +1136,8 @@ optional chain_controller::lookup_linked_permission(account_nam // If no specific or default link found, use active permission if (link != nullptr) { return link->required_permission; - } - return optional(); + } + return optional(); // return optional(); } FC_CAPTURE_AND_RETHROW((authorizer_account)(scope)(act_name)) @@ -1095,8 +1149,8 @@ void chain_controller::validate_uniqueness( const transaction& trx )const { EOS_ASSERT(transaction == nullptr, tx_duplicate, "Transaction is not unique"); } -void chain_controller::record_transaction(const transaction& trx) -{ +void chain_controller::record_transaction(const transaction& trx) +{ try { _db.create([&](transaction_object& transaction) { transaction.trx_id = trx.id(); @@ -1104,10 +1158,10 @@ void chain_controller::record_transaction(const transaction& trx) }); } catch ( ... ) { EOS_ASSERT( false, transaction_exception, - "duplicate transaction ${id}", + "duplicate transaction ${id}", ("id", trx.id() ) ); } -} +} static uint32_t calculate_transaction_cpu_usage( const transaction_trace& trace, const transaction_metadata& meta, const chain_config& chain_configuration ) { // calculate the sum of all actions retired @@ -1149,7 +1203,7 @@ static uint32_t calculate_transaction_cpu_usage( const transaction_trace& trace, context_free_cpu_usage + signature_cpu_usage; - + if( meta.trx().kcpu_usage.value == 0 ) { return actual_usage; @@ -1224,19 +1278,74 @@ void chain_controller::validate_referenced_accounts( const transaction& trx )con } } FC_CAPTURE_AND_RETHROW() } -void chain_controller::validate_expiration( const transaction& trx ) const +void chain_controller::validate_not_expired( const transaction& trx )const { try { fc::time_point now = head_block_time(); + + EOS_ASSERT( now < time_point(trx.expiration), + expired_tx_exception, + "Transaction is expired, now is ${now}, expiration is ${trx.exp}", + ("now",now)("trx.expiration",trx.expiration) ); +} FC_CAPTURE_AND_RETHROW((trx)) } + +void chain_controller::validate_expiration_not_too_far( const transaction& trx, fc::time_point reference_time )const +{ try { const auto& chain_configuration = get_global_properties().configuration; - EOS_ASSERT( time_point(trx.expiration) <= now + fc::seconds(chain_configuration.max_transaction_lifetime), - tx_exp_too_far_exception, "Transaction expiration is too far in the future, expiration is ${trx.expiration} but max expiration is ${max_til_exp}", - ("trx.expiration",trx.expiration)("now",now) - ("max_til_exp",chain_configuration.max_transaction_lifetime)); - EOS_ASSERT( now <= time_point(trx.expiration), expired_tx_exception, "Transaction is expired, now is ${now}, expiration is ${trx.exp}", - ("now",now)("trx.exp",trx.expiration)); + EOS_ASSERT( time_point(trx.expiration) <= reference_time + fc::seconds(chain_configuration.max_transaction_lifetime), + tx_exp_too_far_exception, + "Transaction expiration is too far in the future relative to the reference time of ${reference_time}, " + "expiration is ${trx.expiration} and the maximum transaction lifetime is ${max_til_exp} seconds", + ("trx.expiration",trx.expiration)("reference_time",reference_time) + ("max_til_exp",chain_configuration.max_transaction_lifetime) ); +} FC_CAPTURE_AND_RETHROW((trx)) } + + +void chain_controller::validate_transaction_without_state( const transaction& trx )const +{ try { + EOS_ASSERT( !trx.actions.empty(), tx_no_action, "transaction must have at least one action" ); + + // Check for at least one authorization in the context-aware actions + bool has_auth = false; + for( const auto& act : trx.actions ) { + has_auth |= !act.authorization.empty(); + if( has_auth ) break; + } + EOS_ASSERT( has_auth, tx_no_auths, "transaction must have at least one authorization" ); + + // Check that there are no authorizations in any of the context-free actions + for (const auto &act : trx.context_free_actions) { + EOS_ASSERT( act.authorization.empty(), cfa_irrelevant_auth, "context-free actions cannot require authorization" ); + } } FC_CAPTURE_AND_RETHROW((trx)) } +void chain_controller::validate_transaction_with_minimal_state( const transaction& trx )const +{ try { + validate_transaction_without_state(trx); + validate_not_expired(trx); + validate_tapos(trx); +} FC_CAPTURE_AND_RETHROW((trx)) } + +void chain_controller::validate_transaction_with_minimal_state( const packed_transaction& packed_trx, const transaction* trx_ptr )const +{ try { + transaction temp; + if( trx_ptr == nullptr ) { + temp = packed_trx.get_transaction(); + trx_ptr = &temp; + } + + validate_transaction_with_minimal_state(*trx_ptr); + + // enforce that the header is accurate as a commitment to net_usage + uint32_t cfa_sig_net_usage = (uint32_t)(packed_trx.context_free_data.size() + fc::raw::pack_size(packed_trx.signatures)); + uint32_t net_usage_commitment = trx_ptr->net_usage_words.value * 8U; + uint32_t packed_size = (uint32_t)packed_trx.data.size(); + uint32_t net_usage = cfa_sig_net_usage + packed_size; + EOS_ASSERT(net_usage <= net_usage_commitment, + tx_resource_exhausted, + "Packed Transaction and associated data does not fit into the space committed to by the transaction's header! [usage=${usage},commitment=${commit}]", + ("usage", net_usage)("commit", net_usage_commitment)); +} FC_CAPTURE_AND_RETHROW((packed_trx)) } void chain_controller::require_scope( const scope_name& scope )const { switch( uint64_t(scope) ) { @@ -1406,8 +1515,9 @@ const producer_object& chain_controller::get_producer(const account_name& owner_ const permission_object& chain_controller::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, "Fail to retrieve permission: ${level}", ("level", level) ) } +} EOS_RETHROW_EXCEPTIONS( chain::permission_query_exception, "Failed to retrieve permission: ${level}", ("level", level) ) } uint32_t chain_controller::last_irreversible_block_num() const { return get_dynamic_global_properties().last_irreversible_block_num; @@ -1806,28 +1916,18 @@ static void log_handled_exceptions(const transaction& trx) { } FC_CAPTURE_AND_LOG((trx)); } -transaction_trace chain_controller::__apply_transaction( transaction_metadata& meta ) +transaction_trace chain_controller::__apply_transaction( transaction_metadata& meta ) { try { transaction_trace result(meta.id); - EOS_ASSERT( !meta.trx().actions.empty(), tx_no_action, "transactions require at least one context-aware action" ); - // first check for at least one authorization - bool has_auth = false; - for (const auto& act : meta.trx().actions) - has_auth |= !act.authorization.empty(); - - EOS_ASSERT( has_auth, tx_no_auths, "transactions require at least one authorization" ); - - validate_transaction( meta.trx() ); for (const auto &act : meta.trx().context_free_actions) { - EOS_ASSERT( act.authorization.empty(), cfa_irrelevant_auth, "context-free actions cannot require authorization" ); apply_context context(*this, _db, act, meta); context.context_free = true; context.exec(); fc::move_append(result.action_traces, std::move(context.results.applied_actions)); FC_ASSERT( result.deferred_transaction_requests.size() == 0 ); } - + for (const auto &act : meta.trx().actions) { apply_context context(*this, _db, act, meta); context.exec(); @@ -1995,7 +2095,7 @@ vector chain_controller::_push_deferred_transactions( bool fl candidates.emplace_back(>rx); } - auto deferred_transactions_deadline = fc::time_point::now() + fc::microseconds(config::deffered_transactions_max_time_per_block_us); + auto deferred_transactions_deadline = fc::time_point::now() + fc::microseconds(config::deferred_transactions_max_time_per_block_us); vector res; for (const auto* trx_p: candidates) { if (!is_known_transaction(trx_p->trx_id)) { @@ -2045,7 +2145,6 @@ transaction_trace chain_controller::wrap_transaction_processing( transaction_met auto temp_session = _db.start_undo_session(true); // for now apply the transaction serially but schedule it according to those invariants - validate_referenced_accounts(trx); auto result = trx_processing(data); @@ -2056,7 +2155,7 @@ transaction_trace chain_controller::wrap_transaction_processing( transaction_met record_locks_for_data_access(result, bshard_trace.read_locks, bshard_trace.write_locks); bshard.transactions.emplace_back( result ); - + bshard_trace.append(result); // The transaction applied successfully. Merge its changes into the pending block session. diff --git a/libraries/chain/contracts/chain_initializer.cpp b/libraries/chain/contracts/chain_initializer.cpp index 4286b4d02b2d6497fceda1720fe0b8c80f27ceac..b98b132124fe039af48091e45049a48853fd7d0f 100644 --- a/libraries/chain/contracts/chain_initializer.cpp +++ b/libraries/chain/contracts/chain_initializer.cpp @@ -315,7 +315,7 @@ abi_def chain_initializer::eos_contract_abi(const abi_def& eosio_system_abi) {"block_mroot", "checksum256"}, {"producer", "account_name"}, {"schedule_version", "uint32"}, - {"new_producers", "producer_schedule?"} + {"new_producers", "producer_schedule?"} } }); @@ -330,6 +330,10 @@ abi_def chain_initializer::eos_contract_abi(const abi_def& eosio_system_abi) void chain_initializer::prepare_database( chain_controller& chain, chainbase::database& db) { + /// Reserve id of 0 in permission_index by creating dummy permission_object as the very first object in the index: + db.create([&](permission_object& p) { + }); + /// Create the native contract accounts manually; sadly, we can't run their contracts to make them create themselves auto create_native_account = [this, &chain, &db](account_name name) { db.create([this, &name](account_object& a) { diff --git a/libraries/chain/contracts/eosio_contract.cpp b/libraries/chain/contracts/eosio_contract.cpp index 24b2448d603213363e15b1e147afcaacf2ca7570..6ee11b1c20dc567bf8a63caa731ac090c337c6b4 100644 --- a/libraries/chain/contracts/eosio_contract.cpp +++ b/libraries/chain/contracts/eosio_contract.cpp @@ -29,14 +29,14 @@ namespace eosio { namespace chain { namespace contracts { 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.find(boost::make_tuple(a.permission.actor, a.permission.permission)); + context.db.get(boost::make_tuple(a.permission.actor, a.permission.permission)); } } /** * This method is called assuming precondition_system_newaccount succeeds a */ -void apply_eosio_newaccount(apply_context& context) { +void apply_eosio_newaccount(apply_context& context) { auto create = context.act.data_as(); try { context.require_authorization(create.creator); @@ -49,12 +49,16 @@ void apply_eosio_newaccount(apply_context& context) { auto& db = context.mutable_db; - EOS_ASSERT( create.name.to_string().size() <= 12, action_validate_exception, "account names can only be 12 chars long" ); + auto name_str = name(create.name).to_string(); + + EOS_ASSERT( !create.name.empty(), action_validate_exception, "account name cannot be empty" ); + EOS_ASSERT( name_str.size() <= 12, action_validate_exception, "account names can only be 12 chars long" ); // Check if the creator is privileged const auto &creator = db.get(create.creator); if( !creator.privileged ) { - EOS_ASSERT( name(create.name).to_string().find( "eosio." ) == std::string::npos, action_validate_exception, "only privileged accounts can have names that contain 'eosio.'" ); + EOS_ASSERT( name_str.find( "eosio." ) != 0, action_validate_exception, + "only privileged accounts can have names that start with 'eosio.'" ); } auto existing_account = db.find(create.name); @@ -94,8 +98,10 @@ void apply_eosio_newaccount(apply_context& context) { return result; }; - const auto& owner_permission = create_permission("owner", 0, std::move(create.owner)); - create_permission("active", owner_permission.id, std::move(create.active)); + // 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)); } FC_CAPTURE_AND_RETHROW( (create) ) } @@ -124,7 +130,7 @@ void apply_eosio_setcode(apply_context& context) { // wlog( "set code: ${size}", ("size",act.code.size())); db.modify( account, [&]( auto& a ) { /** TODO: consider whether a microsecond level local timestamp is sufficient to detect code version changes*/ - #warning TODO: update setcode message to include the hash, then validate it in validate + #warning TODO: update setcode message to include the hash, then validate it in validate a.code_version = code_id; // Added resize(0) here to avoid bug in boost vector container a.code.resize( 0 ); @@ -181,18 +187,22 @@ 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& db = context.mutable_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 != update.parent, action_validate_exception, - "Cannot set an authority as its own parent"); + EOS_ASSERT( update.permission.to_string().find( "eosio." ) != 0, action_validate_exception, + "Permission names that start with 'eosio.' are reserved" ); + EOS_ASSERT(update.permission != update.parent, action_validate_exception, "Cannot set an authority as its own parent"); + db.get(update.account); EOS_ASSERT(validate(update.data), action_validate_exception, "Invalid authority: ${auth}", ("auth", update.data)); - if (update.permission == "active") - EOS_ASSERT(update.parent == "owner", action_validate_exception, "Cannot change active authority's parent from owner", ("update.parent", update.parent) ); - if (update.permission == "owner") + if( update.permission == config::active_name ) + EOS_ASSERT(update.parent == config::owner_name, action_validate_exception, "Cannot change active authority's parent from owner", ("update.parent", update.parent) ); + if (update.permission == config::owner_name) EOS_ASSERT(update.parent.empty(), action_validate_exception, "Cannot change owner authority's parent"); - - auto& db = context.mutable_db; + 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(); @@ -206,7 +216,7 @@ void apply_eosio_updateauth(apply_context& context) { 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, - "Fail to retrieve permission for: {\"actor\": \"${actor}\", \"permission\": \"${permission}\" }", + "Failed to retrieve permission for: {\"actor\": \"${actor}\", \"permission\": \"${permission}\" }", ("actor", update.account)("permission", update.parent)); while(current->name != config::owner_name) { @@ -221,13 +231,14 @@ void apply_eosio_updateauth(apply_context& context) { 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); auto permission = db.find(boost::make_tuple(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 != "owner") { + if(update.permission != config::owner_name) { auto& parent = db.get(boost::make_tuple(update.account, update.parent)); parent_id = parent.id; } @@ -237,6 +248,9 @@ void apply_eosio_updateauth(apply_context& context) { "Changing parent authority is not currently supported"); + // TODO: Depending on an implementation detail like sizeof(permission_object) for consensus-affecting side effects like + // RAM usage seems like a bad idea. For example, an upgrade of the implementation of boost::interprocess::vector + // could cause a hardfork unless the old size calculation behavior was also carefully replicated in the same upgrade. int64_t old_size = (int64_t)(sizeof(permission_object) + permission->auth.get_billable_size()); // TODO/QUESTION: If we are updating an existing permission, should we check if the message declared @@ -245,7 +259,7 @@ void apply_eosio_updateauth(apply_context& context) { po.auth = update.data; po.parent = parent_id; po.last_updated = context.controller.head_block_time(); - po.delay = time_point_sec(update.delay.convert_to()); + po.delay = fc::seconds(update.delay); }); int64_t new_size = (int64_t)(sizeof(permission_object) + permission->auth.get_billable_size()); @@ -264,7 +278,7 @@ void apply_eosio_updateauth(apply_context& context) { po.auth = update.data; po.parent = parent_id; po.last_updated = context.controller.head_block_time(); - po.delay = time_point_sec(update.delay.convert_to()); + po.delay = fc::seconds(update.delay); }); resources.add_account_ram_usage( @@ -279,11 +293,15 @@ void apply_eosio_updateauth(apply_context& context) { void apply_eosio_deleteauth(apply_context& context) { auto& resources = context.mutable_controller.get_mutable_resource_limits_manager(); auto remove = context.act.data_as(); - EOS_ASSERT(remove.permission != "active", action_validate_exception, "Cannot delete active authority"); - EOS_ASSERT(remove.permission != "owner", action_validate_exception, "Cannot delete owner authority"); + 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& db = context.mutable_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)); { // Check for children @@ -314,24 +332,24 @@ void apply_eosio_linkauth(apply_context& context) { EOS_ASSERT(!requirement.requirement.empty(), action_validate_exception, "Required permission cannot be empty"); context.require_authorization(requirement.account); - + auto& db = context.mutable_db; const auto *account = db.find(requirement.account); EOS_ASSERT(account != nullptr, account_query_exception, - "Fail to retrieve account: ${account}", ("account", requirement.account)); + "Failed to retrieve account: ${account}", ("account", requirement.account)); // Redundant? const auto *code = db.find(requirement.code); EOS_ASSERT(code != nullptr, account_query_exception, - "Fail to retrieve code for account: ${account}", ("account", requirement.code)); - if( requirement.requirement != N(eosio.any) ) { + "Failed to retrieve code for account: ${account}", ("account", requirement.code)); + if( requirement.requirement != config::eosio_any_name ) { const auto *permission = db.find(requirement.requirement); EOS_ASSERT(permission != nullptr, permission_query_exception, - "Fail to retrieve permission: ${permission}", ("permission", requirement.requirement)); + "Failed to retrieve permission: ${permission}", ("permission", requirement.requirement)); } - + auto link_key = boost::make_tuple(requirement.account, requirement.code, requirement.type); auto link = db.find(link_key); - - if (link) { + + if( link ) { EOS_ASSERT(link->required_permission != requirement.requirement, action_validate_exception, "Attempting to update required authority, but new requirement is same as old"); db.modify(*link, [requirement = requirement.requirement](permission_link_object& link) { @@ -351,7 +369,7 @@ void apply_eosio_linkauth(apply_context& context) { "New Permission Link ${code}::${act} -> ${a}@${p}", _V("code", l.code)("act",l.message_type)("a", l.account)("p",l.required_permission) ); } - } FC_CAPTURE_AND_RETHROW((requirement)) + } FC_CAPTURE_AND_RETHROW((requirement)) } void apply_eosio_unlinkauth(apply_context& context) { @@ -549,7 +567,7 @@ void apply_eosio_vetorecovery(apply_context& context) { void apply_eosio_canceldelay(apply_context& context) { auto cancel = context.act.data_as(); - const auto sender_id = cancel.sender_id.convert_to(); + const auto sender_id = cancel.sender_id; const auto& generated_transaction_idx = context.controller.get_database().get_index(); const auto& generated_index = generated_transaction_idx.indices().get(); const auto& itr = generated_index.lower_bound(boost::make_tuple(config::system_account_name, sender_id)); diff --git a/libraries/chain/include/eosio/chain/authority.hpp b/libraries/chain/include/eosio/chain/authority.hpp index d534e8f3ef73b23fdd72221981ae41238b4dc980..b68ab26cfd67b5d0d6bb7a86b92ddec4ab4e8a5f 100644 --- a/libraries/chain/include/eosio/chain/authority.hpp +++ b/libraries/chain/include/eosio/chain/authority.hpp @@ -6,6 +6,8 @@ #include #include +#include + namespace eosio { namespace chain { @@ -59,6 +61,21 @@ struct shared_authority { } size_t get_billable_size() const { + /** + * public_key_type contains a static_variant and so its size could change if we later wanted to add a new public key type of + * of larger size, thus increasing the returned value from shared_authority::get_billable_size() for old authorities that + * do not even use the new public key type. + * + * Although adding a new public key type is a hardforking change anyway, the current implementation means we would need to: + * - track historical sizes of public_key_type, + * - branch on hardfork versions within this function, and + * - calculate billable size of the authority based on the appropriate historical size of public_key_type, + * all in order to avoid retroactively changing the billable size of authorities. + * TODO: Better implementation of get_billable_size()? + * Perhaps it would require changes to how public_key_type is stored in shared_authority? + * For example: change keys (of type shared_vector) to packed_keys (of type shared_vector) + * which store the packed data of vector, and then charge based on that packed size. + */ return keys.size() * sizeof(key_weight) + accounts.size() * sizeof(permission_level_weight); } }; @@ -76,6 +93,15 @@ inline bool validate( const Authority& auth ) { const key_weight* prev = nullptr; decltype(auth.threshold) total_weight = 0; + static_assert( std::is_same::value && + std::is_same::value && + std::is_same::value && + std::is_same::value, + "unexpected type for threshold and/or weight in authority" ); + + if( ( auth.keys.size() + auth.accounts.size() ) > (1 << 16) ) + return false; // overflow protection (assumes weight_type is uint16_t and threshold is of type uint32_t) + for( const auto& k : auth.keys ) { if( !prev ) prev = &k; else if( prev->key < k.key ) return false; diff --git a/libraries/chain/include/eosio/chain/authority_checker.hpp b/libraries/chain/include/eosio/chain/authority_checker.hpp index 389bbb1c33578167aef97e3ba272618a476571b2..021c7dd9a15a3badf73b240e6905cf75af654d67 100644 --- a/libraries/chain/include/eosio/chain/authority_checker.hpp +++ b/libraries/chain/include/eosio/chain/authority_checker.hpp @@ -6,6 +6,7 @@ #include #include +#include #include @@ -80,12 +81,20 @@ namespace detail { } uint32_t operator()(const permission_level_weight& permission) { if (recursion_depth < checker.recursion_depth_limit) { + checker.permission_visitor.push_undo(); checker.permission_visitor(permission.permission); - if( checker.has_permission( permission.permission.actor ) ) + if( checker.has_permission( permission.permission.actor ) ) { total_weight += permission.weight; - else if( checker.satisfied(permission.permission, recursion_depth + 1) ) + checker.permission_visitor.squash_undo(); + // Satisfied by owner may throw off visitor expectations... + // TODO: Figure out what a permission_visitor actually needs and should expect. + } else if( checker.satisfied(permission.permission, recursion_depth + 1) ) { total_weight += permission.weight; + checker.permission_visitor.squash_undo(); + } else { + checker.permission_visitor.pop_undo(); + } } return total_weight; } @@ -109,8 +118,13 @@ namespace detail { {} bool satisfied(const permission_level& permission, uint16_t depth = 0) { - return has_permission( permission.actor ) || - satisfied(permission_to_authority(permission), depth); + if( has_permission( permission.actor ) ) + return true; + try { + return satisfied(permission_to_authority(permission), depth); + } catch( const permission_query_exception& e ) { + return false; + } } template @@ -132,9 +146,9 @@ namespace detail { // Check all permissions, from highest weight to lowest, seeing if signing_keys satisfies them or not weight_tally_visitor visitor(*this, depth); - for (const auto& permission : permissions) + for( const auto& permission : permissions ) // If we've got enough weight, to satisfy the authority, return! - if (permission.visit(visitor) >= authority.threshold) { + if( permission.visit(visitor) >= authority.threshold ) { KeyReverter.cancel(); return true; } @@ -155,15 +169,24 @@ namespace detail { const PermissionVisitorFunc& get_permission_visitor() { return permission_visitor; } + }; /// authority_checker template auto make_auth_checker(PermissionToAuthorityFunc&& pta, PermissionVisitorFunc&& permission_visitor, - uint16_t recursion_depth_limit, + uint16_t recursion_depth_limit, const flat_set& signing_keys, const flat_set& accounts = flat_set() ) { return authority_checker(std::forward(pta), std::forward(permission_visitor), recursion_depth_limit, signing_keys, accounts); } + class noop_permission_visitor { + public: + void push_undo() {} + void pop_undo() {} + void squash_undo() {} + void operator()(const permission_level& perm_level) {} + }; + } } // namespace eosio::chain diff --git a/libraries/chain/include/eosio/chain/chain_controller.hpp b/libraries/chain/include/eosio/chain/chain_controller.hpp index c36f328c5c80b503941581d6eeffbe4af947d564..3865e41ce093959aa6c154a41e8b2fbfd43f643a 100644 --- a/libraries/chain/include/eosio/chain/chain_controller.hpp +++ b/libraries/chain/include/eosio/chain/chain_controller.hpp @@ -269,7 +269,7 @@ namespace eosio { namespace chain { uint32_t head_block_num()const; block_id_type head_block_id()const; account_name head_block_producer()const; - block_header head_block_header()const; + block_header head_block_header()const; uint32_t last_irreversible_block_num() const; @@ -290,14 +290,14 @@ namespace eosio { namespace chain { * @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 time_point set to the max delay that this authorization requires to complete + * @return fc::microseconds set to the max delay that this authorization requires to complete */ - time_point check_authorization( const vector& actions, - const vector& context_free_actions, - const flat_set& provided_keys, - bool allow_unused_signatures = false, - flat_set provided_accounts = flat_set() - )const; + fc::microseconds check_authorization( const vector& actions, + const vector& context_free_actions, + const flat_set& provided_keys, + bool allow_unused_signatures = false, + flat_set provided_accounts = flat_set() + )const; /** * @param account - the account owner of the permission @@ -339,6 +339,8 @@ namespace eosio { namespace chain { template transaction_trace wrap_transaction_processing( transaction_metadata&& data, TransactionProcessing trx_processing ); + transaction_trace delayed_transaction_processing( const transaction_metadata& mtrx ); + /// Reset the object graph in-memory void _initialize_indexes(); void _initialize_chain(contracts::chain_initializer& starter); @@ -361,39 +363,30 @@ namespace eosio { namespace chain { return f(); } - time_point check_transaction_authorization(const transaction& trx, - const vector& signatures, - const vector& cfd = vector(), - bool allow_unused_signatures = false)const; + fc::microseconds check_transaction_authorization(const transaction& trx, + const vector& signatures, + const vector& cfd = vector(), + bool allow_unused_signatures = false)const; void require_scope(const scope_name& name) const; void require_account(const account_name& name) const; - /** - * This method performs some consistency checks on a transaction. - * @throw transaction_exception if the transaction is invalid - */ - template - void validate_transaction(const T& trx) const { - try { - EOS_ASSERT(trx.actions.size() > 0, transaction_exception, "A transaction must have at least one action"); - - validate_expiration(trx); - validate_uniqueness(trx); - validate_tapos(trx); - - } FC_CAPTURE_AND_RETHROW( (trx) ) } - /// Validate transaction helpers @{ - void validate_uniqueness(const transaction& trx)const; - void validate_tapos(const transaction& trx)const; - void validate_referenced_accounts(const transaction& trx)const; - void validate_expiration(const transaction& trx) const; - void record_transaction(const transaction& trx); - void update_resource_usage( transaction_trace& trace, const transaction_metadata& meta ); + void validate_uniqueness( const transaction& trx )const; + void validate_tapos( const transaction& trx )const; + void validate_referenced_accounts( const transaction& trx )const; + void validate_not_expired( const transaction& trx )const; + void validate_expiration_not_too_far( const transaction& trx, fc::time_point reference_time )const; + void validate_transaction_without_state( const transaction& trx )const; + void validate_transaction_with_minimal_state( const transaction& trx )const; + void validate_transaction_with_minimal_state( const packed_transaction& packed_trx, const transaction* trx_ptr = nullptr )const; /// @} + void record_transaction( const transaction& trx ); + void update_resource_usage( transaction_trace& trace, const transaction_metadata& meta ); + + /** * @brief Find the lowest authority level required for @ref authorizer_account to authorize a message of the * specified type @@ -418,11 +411,11 @@ namespace eosio { namespace chain { bool should_check_for_duplicate_transactions()const { return !(_skip_flags&skip_transaction_dupe_check); } bool should_check_tapos()const { return !(_skip_flags&skip_tapos_check); } + bool should_check_signatures()const { return !(_skip_flags&skip_transaction_signatures); } ///Steps involved in applying a new block ///@{ const producer_object& validate_block_header(uint32_t skip, const signed_block& next_block)const; - const producer_object& _validate_block_header(const signed_block& next_block)const; void create_block_summary(const signed_block& next_block); void update_global_properties(const signed_block& b); @@ -441,7 +434,7 @@ namespace eosio { namespace chain { void _start_pending_cycle(); void _finalize_pending_cycle(); void _apply_cycle_trace( const cycle_trace& trace ); - void _finalize_block( const block_trace& b ); + void _finalize_block( const block_trace& b, const producer_object& signing_producer ); transaction _get_on_block_transaction(); void _apply_on_block_transaction(); diff --git a/libraries/chain/include/eosio/chain/config.hpp b/libraries/chain/include/eosio/chain/config.hpp index e137ae99d80a5daf64f1f0d90e23cbbc38cf0b99..0b59f6861b87294d20d28dcbb60257be172611d4 100644 --- a/libraries/chain/include/eosio/chain/config.hpp +++ b/libraries/chain/include/eosio/chain/config.hpp @@ -26,6 +26,7 @@ const static uint64_t eosio_all_scope = N(eosio.all); const static uint64_t active_name = N(active); const static uint64_t owner_name = N(owner); +const static uint64_t eosio_any_name = N(eosio.any); const static int block_interval_ms = 500; const static int block_interval_us = block_interval_ms*1000; @@ -87,7 +88,7 @@ const static int producer_repetitions = 12; /** * In block production, at the begining of each block we schedule deferred transactions until reach this time */ -const static uint32_t deffered_transactions_max_time_per_block_us = 20*1000; //20ms +const static uint32_t deferred_transactions_max_time_per_block_us = 20*1000; //20ms /** * The number of blocks produced per round is based upon all producers having a chance diff --git a/libraries/chain/include/eosio/chain/contracts/types.hpp b/libraries/chain/include/eosio/chain/contracts/types.hpp index 1994ee2b28b9b553741f9d0f47ccc51bddd8eac0..7002b13975d0f871af081d12d4c9e3bc7a3359c8 100644 --- a/libraries/chain/include/eosio/chain/contracts/types.hpp +++ b/libraries/chain/include/eosio/chain/contracts/types.hpp @@ -150,7 +150,7 @@ struct updateauth { permission_name permission; permission_name parent; authority data; - uint32 delay; + uint32_t delay; static account_name get_account() { return config::system_account_name; @@ -270,7 +270,7 @@ struct vetorecovery { }; struct canceldelay { - uint32 sender_id; + uint128_t sender_id; static account_name get_account() { return config::system_account_name; @@ -282,7 +282,7 @@ struct canceldelay { }; struct mindelay { - uint32 delay; + uint32_t delay; static account_name get_account() { return config::system_account_name; diff --git a/libraries/chain/include/eosio/chain/permission_object.hpp b/libraries/chain/include/eosio/chain/permission_object.hpp index 13da1784b5ba458ea22c03014c2003cc35da12ec..29b03015bb35052932de298167cefdfffbba2de9 100644 --- a/libraries/chain/include/eosio/chain/permission_object.hpp +++ b/libraries/chain/include/eosio/chain/permission_object.hpp @@ -13,49 +13,49 @@ namespace eosio { namespace chain { id_type id; account_name owner; ///< the account this permission belongs to - id_type parent; ///< parent permission + id_type parent; ///< parent permission permission_name name; ///< human-readable name for the permission shared_authority auth; ///< authority required to execute this permission time_point last_updated; ///< the last time this authority was updated - time_point delay; ///< delay associated with this permission + fc::microseconds delay; ///< delay associated with this permission /** * @brief Checks if this permission is equivalent or greater than other * @tparam Index The permission_index - * @return a time_point set to the maximum delay encountered between this and the permission that is other; + * @return a fc::microseconds set to the maximum delay encountered between this and the permission that is other; * empty optional otherwise * * Permissions are organized hierarchically such that a parent permission is strictly more powerful than its * children/grandchildren. This method checks whether this permission is of greater or equal power (capable of - * satisfying) permission @ref other. The returned value is an optional that will indicate the - * maximum delay encountered walking the hierarchy between this permission and other, if other satisfies this, + * satisfying) permission @ref other. The returned value is an optional that will indicate the + * maximum delay encountered walking the hierarchy between this permission and other, if this satisfies other, * otherwise an empty optional is returned. */ template - optional satisfies(const permission_object& other, const Index& permission_index) const { + optional satisfies(const permission_object& other, const Index& permission_index) const { // If the owners are not the same, this permission cannot satisfy other - if (owner != other.owner) - return optional(); + if( owner != other.owner ) + return optional(); - // if other satisfies this permission, then other's delay and this delay will have to contribute + // if this permission satisfies other, then other's delay and this delay will have to contribute auto max_delay = other.delay > delay ? other.delay : delay; // If this permission matches other, or is the immediate parent of other, then this permission satisfies other - if (id == other.id || id == other.parent) - return optional(max_delay); + if( id == other.id || id == other.parent ) + return optional(max_delay); // Walk up other's parent tree, seeing if we find this permission. If so, this permission satisfies other const permission_object* parent = &*permission_index.template get().find(other.parent); - while (parent) { - if (max_delay < parent->delay) + while( parent ) { + if( max_delay < parent->delay ) max_delay = parent->delay; - if (id == parent->parent) - return optional(max_delay); - if (parent->parent._id == 0) - return optional(); + if( id == parent->parent ) + return optional(max_delay); + if( parent->parent._id == 0 ) + return optional(); parent = &*permission_index.template get().find(parent->parent); } // This permission is not a parent of other, and so does not satisfy other - return optional(); + return optional(); } }; diff --git a/libraries/chain/include/eosio/chain/transaction.hpp b/libraries/chain/include/eosio/chain/transaction.hpp index 06828585a0bf45101bd19b84b2883faa904e59df..b75b14256a8d7130627173fbf7ef22f6b11112b4 100644 --- a/libraries/chain/include/eosio/chain/transaction.hpp +++ b/libraries/chain/include/eosio/chain/transaction.hpp @@ -17,7 +17,7 @@ namespace eosio { namespace chain { /** * An action is performed by an actor, aka an account. It may * be created explicitly and authorized by signatures or might be - * generated implicitly by executing application code. + * generated implicitly by executing application code. * * This follows the design pattern of React Flux where actions are * named and then dispatched to one or more action handlers (aka stores). @@ -75,8 +75,8 @@ namespace eosio { namespace chain { /** - * When a transaction is referenced by a block it could imply one of several outcomes which - * describe the state-transition undertaken by the block producer. + * When a transaction is referenced by a block it could imply one of several outcomes which + * describe the state-transition undertaken by the block producer. */ struct transaction_receipt { enum status_enum { @@ -101,11 +101,11 @@ namespace eosio { namespace chain { * * All transactions have an expiration time after which they * may no longer be included in the blockchain. Once a block - * with a block_header::timestamp greater than expiration is + * with a block_header::timestamp greater than expiration is * deemed irreversible, then a user can safely trust the transaction - * will never be included. + * will never be included. * - + * Each region is an independent blockchain, it is included as routing * information for inter-blockchain communication. A contract in this * region might generate or authorize a transaction intended for a foreign @@ -141,7 +141,10 @@ namespace eosio { namespace chain { transaction_id_type id()const; digest_type sig_digest( const chain_id_type& chain_id, const vector& cfd = vector() )const; - flat_set get_signature_keys( const vector& signatures, const chain_id_type& chain_id, const vector& cfd = vector() )const; + flat_set get_signature_keys( const vector& signatures, + const chain_id_type& chain_id, + const vector& cfd = vector(), + bool allow_duplicate_keys = false )const; }; @@ -162,7 +165,7 @@ namespace eosio { namespace chain { const signature_type& sign(const private_key_type& key, const chain_id_type& chain_id); signature_type sign(const private_key_type& key, const chain_id_type& chain_id)const; - flat_set get_signature_keys( const chain_id_type& chain_id )const; + flat_set get_signature_keys( const chain_id_type& chain_id, bool allow_duplicate_keys = false )const; }; struct packed_transaction { @@ -253,6 +256,3 @@ FC_REFLECT_ENUM( eosio::chain::packed_transaction::compression_type, (none)(zlib FC_REFLECT( eosio::chain::packed_transaction, (signatures)(context_free_data)(compression)(data) ) FC_REFLECT_DERIVED( eosio::chain::deferred_transaction, (eosio::chain::transaction), (sender_id)(sender)(payer)(execute_after) ) FC_REFLECT( eosio::chain::deferred_reference, (sender_id)(sender) ) - - - diff --git a/libraries/chain/include/eosio/chain/transaction_metadata.hpp b/libraries/chain/include/eosio/chain/transaction_metadata.hpp index cc71f6f589bbce647ae9eff6c072a78f305d8c1b..f22464693f96e3cbda9cd92b953c178bee88b6af 100644 --- a/libraries/chain/include/eosio/chain/transaction_metadata.hpp +++ b/libraries/chain/include/eosio/chain/transaction_metadata.hpp @@ -44,6 +44,7 @@ class transaction_metadata { uint32_t shard_index = 0; uint32_t bandwidth_usage = 0; time_point published; + fc::microseconds delay; // things for processing deferred transactions optional sender; @@ -54,9 +55,9 @@ class transaction_metadata { size_t raw_size = 0; vector packed_trx; - + // is this transaction implicit - bool is_implicit = false; + bool is_implicit = false; // scopes available to this transaction if we are applying a block optional*> allowed_read_locks; diff --git a/libraries/chain/transaction.cpp b/libraries/chain/transaction.cpp index 4c4529439cdb19e19753fa669a77795ea6e49491..80945dabddc2eafaefdcd6dd05dcdea1e3bf64d3 100644 --- a/libraries/chain/transaction.cpp +++ b/libraries/chain/transaction.cpp @@ -57,7 +57,7 @@ bool transaction_header::verify_reference_block( const block_id_type& reference_ } -transaction_id_type transaction::id() const { +transaction_id_type transaction::id() const { digest_type::encoder enc; fc::raw::pack( enc, *this ); return enc.result(); @@ -73,7 +73,7 @@ digest_type transaction::sig_digest( const chain_id_type& chain_id, const vector return enc.result(); } -flat_set transaction::get_signature_keys( const vector& signatures, const chain_id_type& chain_id, const vector& cfd )const +flat_set transaction::get_signature_keys( const vector& signatures, const chain_id_type& chain_id, const vector& cfd, bool allow_duplicate_keys )const { try { using boost::adaptors::transformed; @@ -85,13 +85,19 @@ flat_set transaction::get_signature_keys( const vector::type::iterator it = recovery_cache.get().find(sig); + public_key_type recov; if(it == recovery_cache.get().end() || it->trx_id != id()) { - public_key_type recov = public_key_type(sig, digest); + recov = public_key_type(sig, digest); recovery_cache.emplace_back( cached_pub_key{id(), recov, sig} ); //could fail on dup signatures; not a problem - recovered_pub_keys.insert(recov); - continue; + } else { + recov = it->pub_key; } - recovered_pub_keys.insert(it->pub_key); + bool successful_insertion = false; + std::tie(std::ignore, successful_insertion) = recovered_pub_keys.insert(recov); + EOS_ASSERT( allow_duplicate_keys || successful_insertion, tx_irrelevant_sig, + "transaction includes more than one signature signed using the same key associated with public key: ${key}", + ("key", recov) + ); } while(recovery_cache.size() > recovery_cache_size) @@ -110,9 +116,9 @@ signature_type signed_transaction::sign(const private_key_type& key, const chain return key.sign(sig_digest(chain_id, context_free_data)); } -flat_set signed_transaction::get_signature_keys( const chain_id_type& chain_id )const +flat_set signed_transaction::get_signature_keys( const chain_id_type& chain_id, bool allow_duplicate_keys )const { - return transaction::get_signature_keys(signatures, chain_id, context_free_data); + return transaction::get_signature_keys(signatures, chain_id, context_free_data, allow_duplicate_keys); } namespace bio = boost::iostreams; diff --git a/libraries/chain/wasm_interface.cpp b/libraries/chain/wasm_interface.cpp index 8ffd0c1b35ebf311962cc6da6aa451704a1b8d6d..9a7956044392c682392c360cdf28c443c107ce65 100644 --- a/libraries/chain/wasm_interface.cpp +++ b/libraries/chain/wasm_interface.cpp @@ -545,7 +545,7 @@ class softfloat_api : public context_aware_api { } int32_t _eosio_f32_trunc_i32s( float af ) { float32_t a = to_softfloat32(af); - if (_eosio_f32_ge(af, 2147483648.0f) || _eosio_f32_le(af, -2147483649.0f)) + if (_eosio_f32_ge(af, 2147483648.0f) || _eosio_f32_le(af, -2147483649.0f)) FC_THROW_EXCEPTION( eosio::chain::wasm_execution_error, "Error, f32.convert_s/i32 overflow" ); if (is_nan(a)) @@ -1066,7 +1066,9 @@ class transaction_api : public context_aware_api { fc::raw::unpack(data, data_len, dtrx); dtrx.sender = context.receiver; dtrx.sender_id = (unsigned __int128)sender_id; - dtrx.execute_after = execute_after; + dtrx.execute_after = std::max( execute_after, + time_point_sec( (context.controller.head_block_time() + fc::seconds(dtrx.delay_sec)) + + fc::microseconds(999'999) ) /* rounds up to nearest second */ ); dtrx.payer = actual_payer; context.execute_deferred(std::move(dtrx)); } FC_CAPTURE_AND_RETHROW((fc::to_hex(data, data_len))); diff --git a/libraries/fc/include/fc/scoped_exit.hpp b/libraries/fc/include/fc/scoped_exit.hpp index 5890e9d310c9eee768533737f50e24a64ae1036c..2ae0db18df9c87e88b3934db5b3f83ba6fb3b8db 100644 --- a/libraries/fc/include/fc/scoped_exit.hpp +++ b/libraries/fc/include/fc/scoped_exit.hpp @@ -9,6 +9,9 @@ namespace fc { scoped_exit( C&& c ):callback( std::forward(c) ){} scoped_exit( scoped_exit&& mv ):callback( std::move( mv.callback ) ){} + scoped_exit( const scoped_exit& ) = delete; + scoped_exit& operator=( const scoped_exit& ) = delete; + void cancel() { canceled = true; } ~scoped_exit() { @@ -17,13 +20,16 @@ namespace fc { } scoped_exit& operator = ( scoped_exit&& mv ) { - callback = std::move(mv.callback); + if( this != &mv ) { + ~scoped_exit(); + callback = std::move(mv.callback); + canceled = mv.canceled; + mv.canceled = true; + } + return *this; } private: - scoped_exit( const scoped_exit& ); - scoped_exit& operator=( const scoped_exit& ); - Callback callback; bool canceled = false; }; diff --git a/tests/api_tests/api_tests.cpp b/tests/api_tests/api_tests.cpp index fa4c7489981c3f9b828ca6d0d8c0d96f78569252..370f7e7d7db54b8128c443b99b1fd9d09f99f129 100644 --- a/tests/api_tests/api_tests.cpp +++ b/tests/api_tests/api_tests.cpp @@ -361,7 +361,7 @@ BOOST_FIXTURE_TEST_CASE(action_tests, TESTER) { try { auto res = push_transaction(trx); BOOST_CHECK_EQUAL(res.status, transaction_receipt::executed); } - + uint32_t now = control->head_block_time().sec_since_epoch(); CALL_TEST_FUNCTION( *this, "test_action", "now", fc::raw::pack(now)); @@ -412,7 +412,7 @@ BOOST_FIXTURE_TEST_CASE(cf_action_tests, TESTER) { try { // need at least one normal action BOOST_CHECK_EXCEPTION(push_transaction(trx), tx_no_action, [](const fc::assert_exception& e) { - return expect_assert_message(e, "transactions require at least one context-aware action"); + return expect_assert_message(e, "transaction must have at least one action"); } ); @@ -425,10 +425,9 @@ BOOST_FIXTURE_TEST_CASE(cf_action_tests, TESTER) { try { // signing a transaction with only context_free_actions should not be allowed auto sigs = trx.sign(get_private_key(N(testapi), "active"), chain_id_type()); - BOOST_CHECK_EXCEPTION(push_transaction(trx), tx_irrelevant_sig, + BOOST_CHECK_EXCEPTION(push_transaction(trx), tx_no_action, [](const fc::exception& e) { - edump((e.what())); - return expect_assert_message(e, "signatures"); + return expect_assert_message(e, "transaction must have at least one action"); } ); @@ -468,7 +467,7 @@ BOOST_FIXTURE_TEST_CASE(cf_action_tests, TESTER) { try { trx.context_free_actions.push_back(act); trx.context_free_data.emplace_back(fc::raw::pack(100)); // verify payload matches context free data trx.context_free_data.emplace_back(fc::raw::pack(200)); - + trx.actions.push_back(act1); // attempt to access non context free api for (uint32_t i = 200; i <= 204; ++i) { @@ -653,13 +652,13 @@ BOOST_FIXTURE_TEST_CASE(transaction_tests, TESTER) { try { { signed_transaction trx; auto tm = test_api_action{}; - action act({}, tm); + action act({}, tm); trx.actions.push_back(act); set_transaction_headers(trx); BOOST_CHECK_EXCEPTION(push_transaction(trx), transaction_exception, [](const fc::exception& e) { - return expect_assert_message(e, "transactions require at least one authorization"); + return expect_assert_message(e, "transaction must have at least one authorization"); } ); } @@ -718,6 +717,13 @@ BOOST_FIXTURE_TEST_CASE(transaction_tests, TESTER) { try { } ); + // test send_transaction_expiring_late + BOOST_CHECK_EXCEPTION(CALL_TEST_FUNCTION( *this, "test_transaction", "send_transaction_expiring_late", fc::raw::pack(N(testapi))), + eosio::chain::transaction_exception, [](const eosio::chain::transaction_exception& e) { + return expect_assert_message(e, "Transaction expiration is too far"); + } + ); + BOOST_REQUIRE_EQUAL( validate(), true ); } FC_LOG_AND_RETHROW() } @@ -1310,7 +1316,7 @@ BOOST_FIXTURE_TEST_CASE(permission_tests, TESTER) { try { auto get_result_uint64 = [&]() -> uint64_t { const auto& db = control->get_database(); const auto* t_id = db.find(boost::make_tuple(N(testapi), N(testapi), N(testapi))); - + FC_ASSERT(t_id != 0, "Table id not found"); const auto& idx = db.get_index(); @@ -1358,18 +1364,16 @@ BOOST_FIXTURE_TEST_CASE(permission_tests, TESTER) { try { } ); - BOOST_CHECK_EXCEPTION(CALL_TEST_FUNCTION( *this, "test_permission", "check_authorization", + CALL_TEST_FUNCTION( *this, "test_permission", "check_authorization", fc::raw::pack( check_auth { .account = N(noname), .permission = N(active), .pubkeys = { get_public_key(N(testapi), "active") } - })), fc::exception, - [](const fc::exception& e) { - return expect_assert_message(e, "unknown key"); - } + }) ); + BOOST_CHECK_EQUAL( uint64_t(0), get_result_uint64() ); CALL_TEST_FUNCTION( *this, "test_permission", "check_authorization", fc::raw::pack( check_auth { @@ -1380,6 +1384,18 @@ BOOST_FIXTURE_TEST_CASE(permission_tests, TESTER) { try { ); BOOST_CHECK_EQUAL( uint64_t(0), get_result_uint64() ); + CALL_TEST_FUNCTION( *this, "test_permission", "check_authorization", + fc::raw::pack( check_auth { + .account = N(testapi), + .permission = N(noname), + .pubkeys = { + get_public_key(N(testapi), "active") + } + }) + ); + BOOST_CHECK_EQUAL( uint64_t(0), get_result_uint64() ); + + /* BOOST_CHECK_EXCEPTION(CALL_TEST_FUNCTION( *this, "test_permission", "check_authorization", fc::raw::pack( check_auth { .account = N(testapi), @@ -1392,6 +1408,7 @@ BOOST_FIXTURE_TEST_CASE(permission_tests, TESTER) { try { return expect_assert_message(e, "unknown key"); } ); + */ } FC_LOG_AND_RETHROW() } diff --git a/tests/chain_tests/auth_tests.cpp b/tests/chain_tests/auth_tests.cpp index e951f2f50511a1057bd17b38b0a11d175d23e18a..3b7afe5e3c754e6a73c175929ac2d45d72afbd21 100644 --- a/tests/chain_tests/auth_tests.cpp +++ b/tests/chain_tests/auth_tests.cpp @@ -273,12 +273,12 @@ try { BOOST_CHECK_EXCEPTION(chain.create_account("aaaaaaaaaaaaa"), action_validate_exception, assert_message_is("account names can only be 12 chars long")); - // Creating account with eosio. prefix with priveleged account + // Creating account with eosio. prefix with privileged account chain.create_account("eosio.test1"); // Creating account with eosio. prefix with non-privileged account, should fail BOOST_CHECK_EXCEPTION(chain.create_account("eosio.test2", "joe"), action_validate_exception, - assert_message_is("only privileged accounts can have names that contain 'eosio.'")); + assert_message_is("only privileged accounts can have names that start with 'eosio.'")); } FC_LOG_AND_RETHROW() } @@ -296,7 +296,7 @@ BOOST_AUTO_TEST_CASE( any_auth ) { try { chain.set_authority("bob", "spending", bob_spending_pub_key, "active"); /// this should fail because spending is not active which is default for reqauth - BOOST_REQUIRE_THROW( chain.push_reqauth("alice", { permission_level{N(alice), "spending"} }, { spending_priv_key }), + BOOST_REQUIRE_THROW( chain.push_reqauth("alice", { permission_level{N(alice), "spending"} }, { spending_priv_key }), tx_irrelevant_auth ); chain.produce_block(); @@ -308,7 +308,7 @@ BOOST_AUTO_TEST_CASE( any_auth ) { try { /// this should succeed because eosio::reqauth is linked to any permission chain.push_reqauth("alice", { permission_level{N(alice), "spending"} }, { spending_priv_key }); - + /// this should fail because bob cannot authorize for alice, the permission given must be one-of alices BOOST_REQUIRE_THROW( chain.push_reqauth("alice", { permission_level{N(bob), "spending"} }, { spending_priv_key }), tx_missing_auth ); diff --git a/tests/chain_tests/block_tests.cpp b/tests/chain_tests/block_tests.cpp index 64d1ac9c46227bc44d65554a52553188f8904d05..856bfb4dbd30e54d3421e190d2e75031d00ef0f2 100644 --- a/tests/chain_tests/block_tests.cpp +++ b/tests/chain_tests/block_tests.cpp @@ -652,6 +652,7 @@ BOOST_AUTO_TEST_CASE(wipe) } } FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_CASE(irrelevant_sig_soft_check) { try { tester chain; @@ -676,14 +677,23 @@ BOOST_AUTO_TEST_CASE(irrelevant_sig_soft_check) { trx.sign( chain.get_private_key( name("random"), "active" ), chain_id_type() ); // Check that it throws for irrelevant signatures - BOOST_CHECK_THROW(chain.push_transaction( trx ), tx_irrelevant_sig); + BOOST_REQUIRE_THROW(chain.push_transaction( trx ), tx_irrelevant_sig); + + // Check that it throws for multiple signatures by the same key + trx.signatures.clear(); + trx.sign( chain.get_private_key( config::system_account_name, "active" ), chain_id_type() ); + trx.sign( chain.get_private_key( config::system_account_name, "active" ), chain_id_type() ); + BOOST_REQUIRE_THROW(chain.push_transaction( trx ), tx_irrelevant_sig); + + // Sign the transaction properly and push to the block + trx.signatures.clear(); + trx.sign( chain.get_private_key( config::system_account_name, "active" ), chain_id_type() ); + chain.push_transaction( trx ); - // Push it through with a skip flag - chain.push_transaction( trx, skip_transaction_signatures); // Produce block so the transaction gets included in the block chain.produce_blocks(); - // Now check that a second blockchain accepts the block with the oversigned transaction + // Now check that a second blockchain accepts the block tester newchain; tester_network net; net.connect_blockchain(chain); @@ -692,5 +702,76 @@ BOOST_AUTO_TEST_CASE(irrelevant_sig_soft_check) { } FC_LOG_AND_RETHROW() } +// +BOOST_AUTO_TEST_CASE(irrelevant_sig_hard_check) { + try { + { + tester chain; + + // Make an account, but add an extra signature to the transaction + signed_transaction trx; + + name new_account_name = name("alice"); + authority owner_auth = authority( chain.get_public_key( new_account_name, "owner" ) ); + + trx.actions.emplace_back( vector{{ config::system_account_name, config::active_name}}, + contracts::newaccount{ + .creator = config::system_account_name, + .name = new_account_name, + .owner = owner_auth, + .active = authority( chain.get_public_key( new_account_name, "active" ) ), + .recovery = authority( chain.get_public_key( new_account_name, "recovery" ) ), + }); + chain.set_transaction_headers(trx); + trx.sign( chain.get_private_key( config::system_account_name, "active" ), chain_id_type() ); + trx.sign( chain.get_private_key( name("random"), "active" ), chain_id_type() ); + + // Force push transaction with irrelevant signatures using a skip flag + chain.push_transaction( trx, skip_transaction_signatures ); + // Produce block so the transaction gets included in the block + chain.produce_blocks(); + + // Now check that a second blockchain rejects the block with the oversigned transaction + tester newchain; + tester_network net; + net.connect_blockchain(chain); + BOOST_CHECK_THROW(net.connect_blockchain(newchain), tx_irrelevant_sig); + } + + { + tester chain; + + // Make an account, but add an extra signature to the transaction + signed_transaction trx; + + name new_account_name = name("alice"); + authority owner_auth = authority( chain.get_public_key( new_account_name, "owner" ) ); + + trx.actions.emplace_back( vector{{ config::system_account_name, config::active_name}}, + contracts::newaccount{ + .creator = config::system_account_name, + .name = new_account_name, + .owner = owner_auth, + .active = authority( chain.get_public_key( new_account_name, "active" ) ), + .recovery = authority( chain.get_public_key( new_account_name, "recovery" ) ), + }); + chain.set_transaction_headers(trx); + trx.sign( chain.get_private_key( config::system_account_name, "active" ), chain_id_type() ); + trx.sign( chain.get_private_key( config::system_account_name, "active" ), chain_id_type() ); + + // Force push transaction with multiple signatures by the same key using a skip flag + chain.push_transaction( trx, skip_transaction_signatures ); + // Produce block so the transaction gets included in the block + chain.produce_blocks(); + + // Now check that a second blockchain rejects the block with the oversigned transaction + tester newchain; + tester_network net; + net.connect_blockchain(chain); + BOOST_CHECK_THROW(net.connect_blockchain(newchain), tx_irrelevant_sig); + } + } FC_LOG_AND_RETHROW() +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/misc_tests.cpp b/tests/tests/misc_tests.cpp index 1afdd5f205d4958199fc7d17619eeffe3afde628..c3cbf58351c6a33228c726c13827b09ccb767780 100644 --- a/tests/tests/misc_tests.cpp +++ b/tests/tests/misc_tests.cpp @@ -86,9 +86,37 @@ BOOST_AUTO_TEST_CASE(deterministic_distributions) struct permission_visitor { std::vector permissions; + std::vector size_stack; + bool _log; + + permission_visitor(bool log = false) : _log(log) {} + void operator()(const permission_level& permission) { permissions.push_back(permission); } + + void push_undo() { + if( _log ) + ilog("push_undo called"); + size_stack.push_back(permissions.size()); + } + + void pop_undo() { + if( _log ) + ilog("pop_undo called"); + FC_ASSERT( size_stack.back() <= permissions.size() && size_stack.size() >= 1, + "invariant failure in test permission_visitor" ); + permissions.erase( permissions.begin() + size_stack.back(), permissions.end() ); + size_stack.pop_back(); + } + + void squash_undo() { + if( _log ) + ilog("squash_undo called"); + FC_ASSERT( size_stack.size() >= 1, "invariant failure in test permission_visitor" ); + size_stack.pop_back(); + } + }; BOOST_AUTO_TEST_CASE(authority_checker) @@ -193,6 +221,33 @@ BOOST_AUTO_TEST_CASE(authority_checker) BOOST_TEST(checker.unused_keys().count(c) == 1); } + A = authority(3, {key_weight{a, 2}, key_weight{b, 1}}, {permission_level_weight{{"hello", "world"}, 3}}); + pv._log = true; + { + pv.permissions.clear(); + pv.size_stack.clear(); + auto checker = make_auth_checker(GetCAuthority, pv, 2, {a, b}); + BOOST_TEST(checker.satisfied(A)); + BOOST_TEST(checker.all_keys_used()); + BOOST_TEST(pv.permissions.size() == 0); + } + { + pv.permissions.clear(); + pv.size_stack.clear(); + auto checker = make_auth_checker(GetCAuthority, pv, 2, {a, b, c}); + BOOST_TEST(checker.satisfied(A)); + BOOST_TEST(!checker.all_keys_used()); + BOOST_TEST(checker.used_keys().size() == 1); + BOOST_TEST(checker.used_keys().count(c) == 1); + BOOST_TEST(checker.unused_keys().size() == 2); + BOOST_TEST(checker.unused_keys().count(a) == 1); + BOOST_TEST(checker.unused_keys().count(b) == 1); + BOOST_TEST(pv.permissions.size() == 1); + BOOST_TEST(pv.permissions.back().actor == "hello"); + BOOST_TEST(pv.permissions.back().permission == "world"); + } + pv._log = false; + A = authority(2, {key_weight{a, 1}, key_weight{b, 1}}, {permission_level_weight{{"hello", "world"}, 1}}); BOOST_TEST(!make_auth_checker(GetCAuthority, pv, 2, {a}).satisfied(A)); BOOST_TEST(!make_auth_checker(GetCAuthority, pv, 2, {b}).satisfied(A));