diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index d46eab07..209416a6 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -169,14 +169,14 @@ namespace graphene { namespace app { void network_broadcast_api::broadcast_transaction(const signed_transaction& trx) { trx.validate(); - _app.chain_database()->check_tansaction_for_duplicated_operations(trx); + database_api( *(_app.chain_database() ) ).check_transaction_for_duplicated_operations(trx); _app.chain_database()->push_transaction(trx); _app.p2p_node()->broadcast_transaction(trx); } fc::variant network_broadcast_api::broadcast_transaction_synchronous(const signed_transaction& trx) { - _app.chain_database()->check_tansaction_for_duplicated_operations(trx); + database_api( *(_app.chain_database() ) ).check_transaction_for_duplicated_operations(trx); fc::promise::ptr prom( new fc::promise() ); broadcast_transaction_with_callback( [=]( const fc::variant& v ){ diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index d3af2f29..4d1b60ea 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include @@ -45,6 +46,44 @@ typedef std::map< std::pair, std::vector > market_queue_type; + +namespace { + + struct proposed_operations_digest_accumulator + { + typedef void result_type; + + void operator()(const graphene::chain::proposal_create_operation& proposal) + { + for (auto& operation: proposal.proposed_ops) + { + if( operation.op.which() != graphene::chain::operation::tag::value + && operation.op.which() != graphene::chain::operation::tag::value ) + proposed_operations_digests.push_back(fc::digest(operation.op)); + } + } + + //empty template method is needed for all other operation types + //we can ignore them, we are interested in only proposal_create_operation + template + void operator()(const T&) + {} + + std::vector proposed_operations_digests; + }; + + std::vector gather_proposed_operations_digests(const graphene::chain::transaction& trx) + { + proposed_operations_digest_accumulator digest_accumulator; + for (auto& operation: trx.operations) + { + operation.visit(digest_accumulator); + } + + return digest_accumulator.proposed_operations_digests; + } +} + namespace graphene { namespace app { class database_api_impl; @@ -70,6 +109,7 @@ class database_api_impl : public std::enable_shared_from_this map> get_block_header_batch(const vector block_nums)const; optional get_block(uint32_t block_num)const; processed_transaction get_transaction( uint32_t block_num, uint32_t trx_in_block )const; + void check_transaction_for_duplicated_operations(const signed_transaction& trx); // Globals chain_property_object get_chain_properties()const; @@ -429,6 +469,39 @@ processed_transaction database_api_impl::get_transaction(uint32_t block_num, uin return opt_block->transactions[trx_num]; } +void database_api::check_transaction_for_duplicated_operations(const signed_transaction& trx) +{ + my->check_transaction_for_duplicated_operations(trx); +} + +void database_api_impl::check_transaction_for_duplicated_operations(const signed_transaction& trx) +{ + const auto& idx = _db.get_index_type(); + const auto& pidx = dynamic_cast&>(idx); + const auto& raidx = pidx.get_secondary_index(); + + auto acc_itr = raidx._account_to_proposals.find( GRAPHENE_WITNESS_ACCOUNT ); + if( acc_itr != raidx._account_to_proposals.end() ) + { + auto& p_set = acc_itr->second; + + std::set existed_operations_digests; + for( auto p_itr = p_set.begin(); p_itr != p_set.end(); ++p_itr ) + { + for( auto& operation : (*p_itr)(_db).proposed_transaction.operations ) + { + existed_operations_digests.insert( fc::digest(operation) ); + } + } + + auto proposed_operations_digests = gather_proposed_operations_digests(trx); + for (auto& digest : proposed_operations_digests) + { + FC_ASSERT(existed_operations_digests.count(digest) == 0, "Proposed operation is already pending for apsproval."); + } + } +} + ////////////////////////////////////////////////////////////////////// // // // Globals // diff --git a/libraries/app/include/graphene/app/database_api.hpp b/libraries/app/include/graphene/app/database_api.hpp index 7b0943e4..2b70d965 100644 --- a/libraries/app/include/graphene/app/database_api.hpp +++ b/libraries/app/include/graphene/app/database_api.hpp @@ -192,6 +192,12 @@ class database_api */ optional get_recent_transaction_by_id( const transaction_id_type& id )const; + /** + * TODO + * + */ + void check_transaction_for_duplicated_operations(const signed_transaction& trx); + ///////////// // Globals // ///////////// @@ -645,6 +651,7 @@ class database_api */ vector get_registered_tournaments(account_id_type account_filter, uint32_t limit) const; + private: std::shared_ptr< database_api_impl > my; }; diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index af50a94b..02fe64f6 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -136,8 +136,6 @@ namespace graphene { namespace chain { void add_checkpoints( const flat_map& checkpts ); const flat_map get_checkpoints()const { return _checkpoints; } bool before_last_checkpoint()const; - - void check_tansaction_for_duplicated_operations(const signed_transaction& trx); bool push_block( const signed_block& b, uint32_t skip = skip_nothing ); processed_transaction push_transaction( const signed_transaction& trx, uint32_t skip = skip_nothing ); diff --git a/tests/tests/network_broadcast_api_tests.cpp b/tests/tests/network_broadcast_api_tests.cpp index 1405fc8c..be42a2a0 100644 --- a/tests/tests/network_broadcast_api_tests.cpp +++ b/tests/tests/network_broadcast_api_tests.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include "../common/database_fixture.hpp" @@ -28,6 +30,27 @@ namespace return transfer; } + sport_create_operation make_sport_create_operation(std::string s1, std::string s2) + { + sport_create_operation op; + op.name = {{ s1, s2 }}; + return op; + } + + betting_market_group_create_operation make_betting_market_group_create(string s1, string s2) + { + betting_market_group_create_operation op; + op.description = {{ s1, s2 }}; + return op; + } + + betting_market_create_operation make_betting_market_operation(string s1, string s2) + { + betting_market_create_operation op; + op.description = {{ s1, s2 }}; + return op; + } + committee_member_create_operation make_committee_member_create_operation(const asset& fee, const account_id_type& member, const string& url) { committee_member_create_operation member_create_operation; @@ -48,6 +71,7 @@ namespace fixture.db.create([&](proposal_object& proposal) { proposal.proposed_transaction = transaction; + proposal.required_active_approvals = { GRAPHENE_WITNESS_ACCOUNT }; }); } @@ -88,18 +112,16 @@ namespace } } -BOOST_FIXTURE_TEST_SUITE( check_tansaction_for_duplicated_operations, database_fixture ) +BOOST_FIXTURE_TEST_SUITE( check_transaction_for_duplicated_operations, database_fixture ) -BOOST_AUTO_TEST_CASE( test_exception_throwing_for_the_same_operation_proposed_twice ) +BOOST_AUTO_TEST_CASE( test_exception_throwing_for_the_same_operation_proposed_for_witness_twice ) { try { - ACTORS((alice)) + create_proposal(*this, {make_sport_create_operation("SPORT1", "S1")}); - create_proposal(*this, {make_transfer_operation(account_id_type(), alice_id, asset(500))}); - - auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(account_id_type(), alice_id, asset(500))}); - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + auto trx = make_signed_transaction_with_proposed_operation(*this, {make_sport_create_operation("SPORT1", "S1")} ); + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -112,10 +134,8 @@ BOOST_AUTO_TEST_CASE( check_passes_without_duplication ) { try { - ACTORS((alice)) - - auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(account_id_type(), alice_id, asset(500))}); - BOOST_CHECK_NO_THROW(db.check_tansaction_for_duplicated_operations(trx)); + auto trx = make_signed_transaction_with_proposed_operation(*this, {make_sport_create_operation("SPORT1", "S1")}); + BOOST_CHECK_NO_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx)); } catch( const fc::exception& e ) { @@ -124,16 +144,14 @@ BOOST_AUTO_TEST_CASE( check_passes_without_duplication ) } } -BOOST_AUTO_TEST_CASE( check_passes_for_the_same_operation_with_different_assets ) +BOOST_AUTO_TEST_CASE( check_passes_for_the_same_operation_with_different_names ) { try { - ACTORS((alice)) + create_proposal(*this, {make_sport_create_operation("SPORT1", "S1")}); - create_proposal(*this, {make_transfer_operation(account_id_type(), alice_id, asset(500))}); - - auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(account_id_type(), alice_id, asset(501))}); - BOOST_CHECK_NO_THROW(db.check_tansaction_for_duplicated_operations(trx)); + auto trx = make_signed_transaction_with_proposed_operation(*this, {make_sport_create_operation("SPORT2", "S2")}); + BOOST_CHECK_NO_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx)); } catch( const fc::exception& e ) { @@ -146,13 +164,11 @@ BOOST_AUTO_TEST_CASE( check_fails_for_duplication_in_transaction_with_several_op { try { - ACTORS((alice)) + create_proposal(*this, {make_sport_create_operation("SPORT1", "S1")}); - create_proposal(*this, {make_transfer_operation(account_id_type(), alice_id, asset(500))}); - - auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(account_id_type(), alice_id, asset(501)), - make_transfer_operation(account_id_type(), alice_id, asset(500))}); //duplicated one - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + auto trx = make_signed_transaction_with_proposed_operation(*this, {make_sport_create_operation("SPORT2", "S2"), + make_sport_create_operation("SPORT1", "S1") }); //duplicated one + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -165,14 +181,12 @@ BOOST_AUTO_TEST_CASE( check_fails_for_duplicated_operation_in_existed_proposal_w { try { - ACTORS((alice)) + create_proposal(*this, {make_sport_create_operation("SPORT1", "S1"), + make_sport_create_operation("SPORT2", "S2") }); //duplicated one - create_proposal(*this, {make_transfer_operation(account_id_type(), alice_id, asset(499)), - make_transfer_operation(account_id_type(), alice_id, asset(500))}); //duplicated one - - auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(account_id_type(), alice_id, asset(501)), - make_transfer_operation(account_id_type(), alice_id, asset(500))}); //duplicated one - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + auto trx = make_signed_transaction_with_proposed_operation(*this, {make_sport_create_operation("SPORT3", "S3"), + make_sport_create_operation("SPORT2", "S2")}); //duplicated one + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -185,13 +199,11 @@ BOOST_AUTO_TEST_CASE( check_fails_for_duplicated_operation_in_existed_proposal_w { try { - ACTORS((alice)) + create_proposal(*this, {make_sport_create_operation("SPORT1", "S1"), + make_sport_create_operation("SPORT2", "S2")}); //duplicated one - create_proposal(*this, {make_transfer_operation(account_id_type(), alice_id, asset(499)), - make_transfer_operation(account_id_type(), alice_id, asset(500))}); //duplicated one - - auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(account_id_type(), alice_id, asset(500))}); //duplicated one - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + auto trx = make_signed_transaction_with_proposed_operation(*this, {make_sport_create_operation("SPORT2", "S2")}); //duplicated one + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -204,12 +216,12 @@ BOOST_AUTO_TEST_CASE( check_passes_for_different_operations_types ) { try { - ACTORS((alice)) + ACTOR( alice ); create_proposal(*this, {make_transfer_operation(account_id_type(), alice_id, asset(500))}); auto trx = make_signed_transaction_with_proposed_operation(*this, {make_committee_member_create_operation(asset(1000), account_id_type(), "test url")}); - BOOST_CHECK_NO_THROW(db.check_tansaction_for_duplicated_operations(trx)); + BOOST_CHECK_NO_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx)); } catch( const fc::exception& e ) { @@ -225,7 +237,7 @@ BOOST_AUTO_TEST_CASE( check_fails_for_same_member_create_operations ) create_proposal(*this, {make_committee_member_create_operation(asset(1000), account_id_type(), "test url")}); auto trx = make_signed_transaction_with_proposed_operation(*this, {make_committee_member_create_operation(asset(1000), account_id_type(), "test url")}); - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -241,7 +253,7 @@ BOOST_AUTO_TEST_CASE( check_passes_for_different_member_create_operations ) create_proposal(*this, {make_committee_member_create_operation(asset(1000), account_id_type(), "test url")}); auto trx = make_signed_transaction_with_proposed_operation(*this, {make_committee_member_create_operation(asset(1001), account_id_type(), "test url")}); - BOOST_CHECK_NO_THROW(db.check_tansaction_for_duplicated_operations(trx)); + BOOST_CHECK_NO_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx)); } catch( const fc::exception& e ) { @@ -265,7 +277,7 @@ BOOST_AUTO_TEST_CASE( check_failes_for_several_operations_of_mixed_type ) auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(account_id_type(), alice_id, asset(501)), //duplicate make_committee_member_create_operation(asset(1002), account_id_type(), "test url")}); - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -278,20 +290,15 @@ BOOST_AUTO_TEST_CASE( check_failes_for_duplicates_in_pending_transactions_list ) { try { - ACTORS((alice)) + ACTOR( alice ); - fc::ecc::private_key committee_key = init_account_priv_key; + auto duplicate = make_sport_create_operation("SPORT1", "S1"); - const account_object& moneyman = create_account("moneyman", init_account_pub_key); - const asset_object& core = asset_id_type()(db); + push_proposal( *this, GRAPHENE_WITNESS_ACCOUNT(db), {duplicate} ); - transfer(account_id_type()(db), moneyman, core.amount(1000000)); + auto trx = make_signed_transaction_with_proposed_operation( *this, {duplicate} ); - auto duplicate = make_transfer_operation(alice.id, moneyman.get_id(), asset(100)); - push_proposal(*this, moneyman, {duplicate}); - - auto trx = make_signed_transaction_with_proposed_operation(*this, {duplicate}); - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -316,7 +323,7 @@ BOOST_AUTO_TEST_CASE( check_passes_for_no_duplicates_in_pending_transactions_lis push_proposal(*this, moneyman, {make_transfer_operation(alice.id, moneyman.get_id(), asset(100))}); auto trx = make_signed_transaction_with_proposed_operation(*this, {make_transfer_operation(alice.id, moneyman.get_id(), asset(101))}); - BOOST_CHECK_NO_THROW(db.check_tansaction_for_duplicated_operations(trx)); + BOOST_CHECK_NO_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx)); } catch( const fc::exception& e ) { @@ -338,13 +345,13 @@ BOOST_AUTO_TEST_CASE( check_fails_for_several_transactions_with_duplicates_in_pe transfer(account_id_type()(db), moneyman, core.amount(1000000)); - auto duplicate = make_transfer_operation(alice.id, moneyman.get_id(), asset(100)); - push_proposal(*this, moneyman, {make_transfer_operation(alice.id, moneyman.get_id(), asset(101)), - duplicate}); + auto duplicate = make_sport_create_operation("SPORT1", "S1"); + push_proposal(*this, moneyman, {make_sport_create_operation("SPORT2", "S2"), duplicate} ); - auto trx = make_signed_transaction_with_proposed_operation(*this, {duplicate, - make_transfer_operation(alice.id, moneyman.get_id(), asset(102))}); - BOOST_CHECK_THROW(db.check_tansaction_for_duplicated_operations(trx), fc::exception); + auto trx = make_signed_transaction_with_proposed_operation(*this, + {duplicate, make_sport_create_operation("SPORT3", "S3")} ); + + BOOST_CHECK_THROW(graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx), fc::exception); } catch( const fc::exception& e ) { @@ -353,61 +360,80 @@ BOOST_AUTO_TEST_CASE( check_fails_for_several_transactions_with_duplicates_in_pe } } -BOOST_AUTO_TEST_CASE( check_passes_for_duplicated_betting_market_or_group ) +BOOST_AUTO_TEST_CASE( check_passes_for_duplicated_betting_market_group_create ) { - generate_blocks( HARDFORK_1000_TIME + fc::seconds(300) ); + try + { + auto duplicate = make_betting_market_group_create( "BMGROUP1", "BMG1" ); + create_proposal(*this, {duplicate} ); + + auto trx = make_signed_transaction_with_proposed_operation(*this, {duplicate} ); + + BOOST_CHECK_NO_THROW( graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx) ); + } + catch( const fc::exception &e ) + { + edump( ( e.to_detail_string() ) ); + throw; + } +} + +BOOST_AUTO_TEST_CASE( check_passes_for_duplicated_betting_market_create ) +{ try { - const sport_id_type sport_id = create_sport( {{"SN","SPORT_NAME"}} ).id; - const event_group_id_type event_group_id = create_event_group( {{"EG", "EVENT_GROUP"}}, sport_id ).id; - const betting_market_rules_id_type betting_market_rules_id = - create_betting_market_rules( {{"EN", "Rules"}}, {{"EN", "Some rules"}} ).id; - - event_create_operation evcop1; - evcop1.event_group_id = event_group_id; - evcop1.name = {{"NO", "NAME_ONE"}}; - evcop1.season = {{"NO", "NAME_ONE"}}; + auto duplicate = make_betting_market_operation( "BMARKET1", "BM1" ); - event_create_operation evcop2; - evcop2.event_group_id = event_group_id; - evcop2.name = {{"NT", "NAME_TWO"}}; - evcop2.season = {{"NT", "NAME_TWO"}}; + create_proposal( *this, {duplicate} ); - betting_market_group_create_operation bmgcop; - bmgcop.description = {{"NN", "NO_NAME"}}; - bmgcop.event_id = object_id_type(relative_protocol_ids, 0, 0); - bmgcop.rules_id = betting_market_rules_id; - bmgcop.asset_id = asset_id_type(); + auto trx = make_signed_transaction_with_proposed_operation(*this, {duplicate} ); - betting_market_create_operation bmcop; - bmcop.group_id = object_id_type(relative_protocol_ids, 0, 1); - bmcop.payout_condition.insert( internationalized_string_type::value_type( "CN", "CONDI_NAME" ) ); - - proposal_create_operation pcop1 = proposal_create_operation::committee_proposal( - db.get_global_properties().parameters, - db.head_block_time() - ); - pcop1.review_period_seconds.reset(); - - proposal_create_operation pcop2 = pcop1; - - pcop1.proposed_ops.emplace_back( evcop1 ); - pcop1.proposed_ops.emplace_back( bmgcop ); - pcop1.proposed_ops.emplace_back( bmcop ); - - pcop2.proposed_ops.emplace_back( evcop2 ); - pcop2.proposed_ops.emplace_back( bmgcop ); - pcop2.proposed_ops.emplace_back( bmcop ); - - create_proposal(*this, { pcop1, pcop2 }); - - auto trx = make_signed_transaction_with_proposed_operation(*this, { pcop1, pcop2 }); - BOOST_CHECK_NO_THROW( db.check_tansaction_for_duplicated_operations(trx) ); + BOOST_CHECK_NO_THROW( graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx) ); } - catch( const fc::exception& e ) + catch( const fc::exception &e ) { - edump((e.to_detail_string())); + edump( ( e.to_detail_string() ) ); + throw; + } +} + +BOOST_AUTO_TEST_CASE( check_passes_for_duplicated_betting_market_and_betting_market_group_create ) +{ + try + { + auto duplicate_market = make_betting_market_operation( "BMARKET1", "BM1" ); + auto duplicate_group = make_betting_market_group_create( "BMGROUP1", "BMG1" ); + + create_proposal( *this, {duplicate_market, duplicate_group} ); + + auto trx = make_signed_transaction_with_proposed_operation(*this, {duplicate_market, duplicate_group} ); + + BOOST_CHECK_NO_THROW( graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx) ); + } + catch( const fc::exception &e ) + { + edump( ( e.to_detail_string() ) ); + throw; + } +} + +BOOST_AUTO_TEST_CASE( check_passes_for_duplicated_betting_market_in_one_operation ) +{ + try + { + auto duplicate = make_betting_market_operation( "BMARKET1", "BM1" ); + + create_proposal( *this, {duplicate, duplicate} ); + + auto trx = make_signed_transaction_with_proposed_operation(*this, {duplicate, duplicate} ); + + BOOST_CHECK_NO_THROW( graphene::app::database_api(db).check_transaction_for_duplicated_operations(trx) ); + } + catch( const fc::exception &e ) + { + edump( ( e.to_detail_string() ) ); + throw; } }