diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 217b2ff862..8b738bd959 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -376,6 +376,19 @@ class wallet_api */ signed_transaction sign_builder_transaction(transaction_handle_type transaction_handle, bool broadcast = true); + /** + * @ingroup Transaction Builder API + * + * Sign the transaction in a transaction builder and optionally broadcast to the network. + * @param transaction_handle handle of the transaction builder + * @param signing_keys Keys that must be used when signing the transaction + * @param broadcast whether to broadcast the signed transaction to the network + * @return a signed transaction + */ + signed_transaction sign_builder_transaction2(transaction_handle_type transaction_handle, + const vector& signing_keys = vector(), + bool broadcast = true); + /** Broadcast signed transaction * @param tx signed transaction * @returns the transaction ID along with the signed transaction. @@ -1588,6 +1601,21 @@ class wallet_api */ signed_transaction sign_transaction(signed_transaction tx, bool broadcast = false); + /** Signs a transaction. + * + * Given a fully-formed transaction that is only lacking signatures, this signs + * the transaction with the inferred necessary keys and the explicitly provided keys, + * and optionally broadcasts the transaction + * @param tx the unsigned transaction + * @param signing_keys Keys that must be used when signing the transaction + * @param broadcast true if you wish to broadcast the transaction + * @return the signed version of the transaction + */ + signed_transaction sign_transaction2(signed_transaction tx, + const vector& signing_keys = vector(), + bool broadcast = true); + + /** Get transaction signers. * * Returns information about who signed the transaction, specifically, @@ -1769,6 +1797,7 @@ FC_API( graphene::wallet::wallet_api, (set_fees_on_builder_transaction) (preview_builder_transaction) (sign_builder_transaction) + (sign_builder_transaction2) (broadcast_transaction) (propose_builder_transaction) (propose_builder_transaction2) @@ -1857,6 +1886,7 @@ FC_API( graphene::wallet::wallet_api, (save_wallet_file) (serialize_transaction) (sign_transaction) + (sign_transaction2) (add_transaction_signature) (get_transaction_signers) (get_key_references) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index f2b753a778..3c73d7530f 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -526,6 +526,13 @@ signed_transaction wallet_api::sign_builder_transaction(transaction_handle_type return my->sign_builder_transaction(transaction_handle, broadcast); } +signed_transaction wallet_api::sign_builder_transaction2(transaction_handle_type transaction_handle, + const vector& explicit_keys, + bool broadcast) +{ + return my->sign_builder_transaction2(transaction_handle, explicit_keys, broadcast); +} + pair wallet_api::broadcast_transaction(signed_transaction tx) { return my->broadcast_transaction(tx); @@ -999,6 +1006,12 @@ signed_transaction wallet_api::sign_transaction(signed_transaction tx, bool broa return my->sign_transaction( tx, broadcast); } FC_CAPTURE_AND_RETHROW( (tx) ) } +signed_transaction wallet_api::sign_transaction2(signed_transaction tx, const vector& signing_keys, + bool broadcast /* = false */) +{ try { + return my->sign_transaction2( tx, signing_keys, broadcast); +} FC_CAPTURE_AND_RETHROW( (tx) ) } + flat_set wallet_api::get_transaction_signers(const signed_transaction &tx) const { try { return my->get_transaction_signers(tx); diff --git a/libraries/wallet/wallet_api_impl.hpp b/libraries/wallet/wallet_api_impl.hpp index 6bbb50acbc..465c071f0b 100644 --- a/libraries/wallet/wallet_api_impl.hpp +++ b/libraries/wallet/wallet_api_impl.hpp @@ -219,6 +219,9 @@ class wallet_api_impl asset set_fees_on_builder_transaction(transaction_handle_type handle, string fee_asset = GRAPHENE_SYMBOL); transaction preview_builder_transaction(transaction_handle_type handle); signed_transaction sign_builder_transaction(transaction_handle_type transaction_handle, bool broadcast = true); + signed_transaction sign_builder_transaction2(transaction_handle_type transaction_handle, + const vector& signing_keys = vector(), + bool broadcast = true); pair broadcast_transaction(signed_transaction tx); @@ -325,6 +328,9 @@ class wallet_api_impl bool broadcast ); signed_transaction sign_transaction(signed_transaction tx, bool broadcast = false); + signed_transaction sign_transaction2(signed_transaction tx, + const vector& signing_keys = vector(), + bool broadcast = false); flat_set get_transaction_signers(const signed_transaction &tx) const; diff --git a/libraries/wallet/wallet_builder.cpp b/libraries/wallet/wallet_builder.cpp index dbcbe12c5d..5dd7f00955 100644 --- a/libraries/wallet/wallet_builder.cpp +++ b/libraries/wallet/wallet_builder.cpp @@ -88,6 +88,15 @@ namespace graphene { namespace wallet { namespace detail { sign_transaction(_builder_transactions[transaction_handle], broadcast); } + signed_transaction wallet_api_impl::sign_builder_transaction2(transaction_handle_type + transaction_handle, const vector& signing_keys, bool broadcast) + { + FC_ASSERT(_builder_transactions.count(transaction_handle)); + + return _builder_transactions[transaction_handle] = + sign_transaction2(_builder_transactions[transaction_handle], signing_keys, broadcast); + } + signed_transaction wallet_api_impl::propose_builder_transaction( transaction_handle_type handle, time_point_sec expiration, uint32_t review_period_seconds, bool broadcast) { diff --git a/libraries/wallet/wallet_sign.cpp b/libraries/wallet/wallet_sign.cpp index 5299099752..08722ed63b 100644 --- a/libraries/wallet/wallet_sign.cpp +++ b/libraries/wallet/wallet_sign.cpp @@ -311,9 +311,20 @@ namespace graphene { namespace wallet { namespace detail { } signed_transaction wallet_api_impl::sign_transaction( signed_transaction tx, bool broadcast ) + { + return sign_transaction2(tx, {}, broadcast); + } + + signed_transaction wallet_api_impl::sign_transaction2( signed_transaction tx, + const vector& signing_keys, bool broadcast) { set approving_key_set = get_owned_required_keys(tx); + // Add any explicit keys to the approving_key_set + for (const public_key_type& explicit_key : signing_keys) { + approving_key_set.insert(explicit_key); + } + auto dyn_props = get_dynamic_global_properties(); tx.set_reference_block( dyn_props.head_block_id ); diff --git a/tests/cli/main.cpp b/tests/cli/main.cpp index baf3852ba7..9ef11e4295 100644 --- a/tests/cli/main.cpp +++ b/tests/cli/main.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -174,12 +175,46 @@ bool generate_block(std::shared_ptr app, graphene::c } } -bool generate_block(std::shared_ptr app) +bool generate_block(std::shared_ptr app) { graphene::chain::signed_block returned_block; return generate_block(app, returned_block); } + +signed_block generate_block(std::shared_ptr app, uint32_t skip, const fc::ecc::private_key& key, int miss_blocks) +{ + // skip == ~0 will skip checks specified in database::validation_steps + skip |= database::skip_undo_history_check; + + auto db = app->chain_database(); + auto block = db->generate_block(db->get_slot_time(miss_blocks + 1), + db->get_scheduled_witness(miss_blocks + 1), + key, skip); + db->clear_pending(); + return block; +} + + +////// +// Generate blocks until the timestamp +////// +uint32_t generate_blocks(std::shared_ptr app, fc::time_point_sec timestamp) +{ + fc::ecc::private_key committee_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan"))); + uint32_t skip = ~0; + auto db = app->chain_database(); + + generate_block(app); + auto slots_to_miss = db->get_slot_at_time(timestamp); + if( slots_to_miss <= 1 ) + return 1; + --slots_to_miss; + generate_block(app, skip, committee_key, slots_to_miss); + return 2; +} + + /////////// /// @brief Skip intermediate blocks, and generate a maintenance block /// @param app the application @@ -215,7 +250,8 @@ class client_connection client_connection( std::shared_ptr app, const fc::temp_directory& data_dir, - const int server_port_number + const int server_port_number, + const std::string custom_wallet_filename = "wallet.json" ) { wallet_data.chain_id = app->chain_database()->get_chain_id(); @@ -231,7 +267,7 @@ class client_connection BOOST_CHECK(remote_login_api->login( wallet_data.ws_user, wallet_data.ws_password ) ); wallet_api_ptr = std::make_shared(wallet_data, remote_login_api); - wallet_filename = data_dir.path().generic_string() + "/wallet.json"; + wallet_filename = data_dir.path().generic_string() + "/" + custom_wallet_filename; wallet_api_ptr->set_wallet_filename(wallet_filename); wallet_api = fc::api(wallet_api_ptr); @@ -239,17 +275,10 @@ class client_connection wallet_cli = std::make_shared(GRAPHENE_MAX_NESTED_OBJECTS); for( auto& name_formatter : wallet_api_ptr->get_result_formatters() ) wallet_cli->format_result( name_formatter.first, name_formatter.second ); - - boost::signals2::scoped_connection closed_connection(websocket_connection->closed.connect([=]{ - cerr << "Server has disconnected us.\n"; - wallet_cli->stop(); - })); - (void)(closed_connection); } ~client_connection() { - // wait for everything to finish up - fc::usleep(fc::milliseconds(500)); + wallet_cli->stop(); } public: fc::http::websocket_client websocket_client; @@ -316,10 +345,6 @@ struct cli_fixture ~cli_fixture() { BOOST_TEST_MESSAGE("Cleanup cli_wallet::boost_fixture_test_case"); - - // wait for everything to finish up - fc::usleep(fc::seconds(1)); - app1->shutdown(); #ifdef _WIN32 sockQuit(); @@ -495,6 +520,122 @@ BOOST_FIXTURE_TEST_CASE( cli_get_signed_transaction_signers, cli_fixture ) } } + +/////////////////////// +// Wallet RPC +// Test adding an unnecessary signature to a transaction +/////////////////////// +BOOST_FIXTURE_TEST_CASE(cli_sign_tx_with_unnecessary_signature, cli_fixture) { + try { + auto db = app1->chain_database(); + + account_object nathan_acct = con.wallet_api_ptr->get_account("nathan"); + INVOKE(upgrade_nathan_account); + + // Register Bob account + const auto bob_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "bob", bob_bki.pub_key, bob_bki.pub_key, "nathan", "nathan", 0, true + ); + + // Register Charlie account + const graphene::wallet::brain_key_info charlie_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "charlie", charlie_bki.pub_key, charlie_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &charlie_acc = con.wallet_api_ptr->get_account("charlie"); + + // Import Bob's key + BOOST_CHECK(con.wallet_api_ptr->import_key("bob", bob_bki.wif_priv_key)); + + // Create transaction with a transfer operation from Nathan to Charlie + transfer_operation top; + top.from = nathan_acct.id; + top.to = charlie_acc.id; + top.amount = asset(5000); + top.fee = db->current_fee_schedule().calculate_fee(top); + + signed_transaction test_tx; + test_tx.operations.push_back(top); + + // Sign the transaction with the implied nathan's key and the explicitly yet unnecessary Bob's key + auto signed_trx = con.wallet_api_ptr->sign_transaction2(test_tx, {bob_bki.pub_key}, false); + + // Check for two signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 2); + flat_set signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + + // Check that the signed transaction contains both Nathan's required signature and Bob's unnecessary signature + BOOST_CHECK_EQUAL(nathan_acct.active.get_keys().size(), 1); + flat_set expected_signers = {bob_bki.pub_key, nathan_acct.active.get_keys().front()}; + flat_set actual_signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(signers == expected_signers); + + } catch (fc::exception &e) { + edump((e.to_detail_string())); + throw; + } +} + + +/////////////////////// +// Wallet RPC +// Test adding an unnecessary signature to a transaction builder +/////////////////////// +BOOST_FIXTURE_TEST_CASE(cli_sign_tx_builder_with_unnecessary_signature, cli_fixture) { + try { + auto db = app1->chain_database(); + + account_object nathan_acct = con.wallet_api_ptr->get_account("nathan"); + INVOKE(upgrade_nathan_account); + + // Register Bob account + const auto bob_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "bob", bob_bki.pub_key, bob_bki.pub_key, "nathan", "nathan", 0, true + ); + + // Register Charlie account + const graphene::wallet::brain_key_info charlie_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "charlie", charlie_bki.pub_key, charlie_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &charlie_acc = con.wallet_api_ptr->get_account("charlie"); + + // Import Bob's key + BOOST_CHECK(con.wallet_api_ptr->import_key("bob", bob_bki.wif_priv_key)); + + // Use transaction builder to build a transaction with a transfer operation from Nathan to Charlie + graphene::wallet::transaction_handle_type tx_handle = con.wallet_api_ptr->begin_builder_transaction(); + + transfer_operation top; + top.from = nathan_acct.id; + top.to = charlie_acc.id; + top.amount = asset(5000); + + con.wallet_api_ptr->add_operation_to_builder_transaction(tx_handle, top); + con.wallet_api_ptr->set_fees_on_builder_transaction(tx_handle, GRAPHENE_SYMBOL); + + // Sign the transaction with the implied nathan's key and the explicitly yet unnecessary Bob's key + auto signed_trx = con.wallet_api_ptr->sign_builder_transaction2(tx_handle, {bob_bki.pub_key}, false); + + // Check for two signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 2); + flat_set signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + + // Check that the signed transaction contains both Nathan's required signature and Bob's unnecessary signature + BOOST_CHECK_EQUAL(nathan_acct.active.get_keys().size(), 1); + flat_set expected_signers = {bob_bki.pub_key, nathan_acct.active.get_keys().front()}; + flat_set actual_signers = con.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(signers == expected_signers); + + } catch (fc::exception &e) { + edump((e.to_detail_string())); + throw; + } +} + + BOOST_FIXTURE_TEST_CASE( cli_get_available_transaction_signers, cli_fixture ) { try @@ -1264,3 +1405,182 @@ BOOST_FIXTURE_TEST_CASE( general_storage, cli_fixture ) throw; } } + +////// +// Template copied +////// +template +unsigned_int member_index(string name) { + unsigned_int index; + fc::typelist::runtime::for_each(typename fc::reflector::native_members(), [&name, &index](auto t) mutable { + if (name == decltype(t)::type::get_name()) + index = decltype(t)::type::index; + }); + return index; +} + +/////////////////////// +// Wallet RPC +// Test sign_builder_transaction2 with an account (bob) that has received a custom authorization +// to transfer funds from another account (alice) +/////////////////////// +BOOST_FIXTURE_TEST_CASE(cli_use_authorized_transfer, cli_fixture) { + try { + ////// + // Initialize the blockchain + ////// + auto db = app1->chain_database(); + + account_object nathan_acct = con.wallet_api_ptr->get_account("nathan"); + INVOKE(upgrade_nathan_account); + + // Register Alice account + const auto alice_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "alice", alice_bki.pub_key, alice_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &alice_acc = con.wallet_api_ptr->get_account("alice"); + + // Register Bob account + const auto bob_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "bob", bob_bki.pub_key, bob_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &bob_acc = con.wallet_api_ptr->get_account("bob"); + + // Register Charlie account + const graphene::wallet::brain_key_info charlie_bki = con.wallet_api_ptr->suggest_brain_key(); + con.wallet_api_ptr->register_account( + "charlie", charlie_bki.pub_key, charlie_bki.pub_key, "nathan", "nathan", 0, true + ); + const account_object &charlie_acc = con.wallet_api_ptr->get_account("charlie"); + + // Fund Alice's account + con.wallet_api_ptr->transfer("nathan", "alice", "450000", "1.3.0", "", true); + + // Initialize common variables + signed_transaction signed_trx; + + + ////// + // Initialize Alice's CLI wallet + ////// + client_connection con_alice(app1, app_dir, server_port_number, "wallet_alice.json"); + con_alice.wallet_api_ptr->set_password("supersecret"); + con_alice.wallet_api_ptr->unlock("supersecret"); + + // Import Alice's key + BOOST_CHECK(con_alice.wallet_api_ptr->import_key("alice", alice_bki.wif_priv_key)); + + + ////// + // Initialize the blockchain for BSIP 40 + ////// + generate_blocks(app1, HARDFORK_BSIP_40_TIME); + // Set committee parameters + app1->chain_database()->modify(app1->chain_database()->get_global_properties(), [](global_property_object& p) { + p.parameters.extensions.value.custom_authority_options = custom_authority_options_type(); + }); + + + ////// + // Alice authorizes Bob to transfer funds from her account to Charlie's account + ////// + graphene::wallet::transaction_handle_type tx_alice_handle = con_alice.wallet_api_ptr->begin_builder_transaction(); + + custom_authority_create_operation caop; + caop.account = alice_acc.get_id(); + caop.auth.add_authority(bob_acc.get_id(), 1); + caop.auth.weight_threshold = 1; + caop.enabled = true; + caop.valid_to = db->head_block_time() + 1000; + caop.operation_type = operation::tag::value; + + // Restriction should have "to" equal Charlie + vector restrictions; + auto to_index = member_index("to"); + restrictions.emplace_back(to_index, restriction::func_eq, charlie_acc.get_id()); + + con_alice.wallet_api_ptr->add_operation_to_builder_transaction(tx_alice_handle, caop); + asset ca_fee = con_alice.wallet_api_ptr->set_fees_on_builder_transaction(tx_alice_handle, GRAPHENE_SYMBOL); + + // Sign the transaction with the inferred Alice key + signed_trx = con_alice.wallet_api_ptr->sign_builder_transaction2(tx_alice_handle, {}, true); + + // Check for one signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 1); + + // Check that the signed transaction contains Alice's signature + flat_set expected_signers = {alice_bki.pub_key}; + flat_set actual_signers = con_alice.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(actual_signers == expected_signers); + + + ////// + // Initialize Bob's CLI wallet + ////// + client_connection con_bob(app1, app_dir, server_port_number, "wallet_bob.json"); + con_bob.wallet_api_ptr->set_password("supersecret"); + con_bob.wallet_api_ptr->unlock("supersecret"); + + // Import Bob's key + BOOST_CHECK(con_bob.wallet_api_ptr->import_key("bob", bob_bki.wif_priv_key)); + + + ////// + // Bob attempt to transfer funds from Alice to Charlie while using Bob's wallet + // This should succeed because Bob is authorized to transfer by Alice + ////// + graphene::wallet::transaction_handle_type tx_bob_handle = con_bob.wallet_api_ptr->begin_builder_transaction(); + + const asset transfer_amount = asset(123 * GRAPHENE_BLOCKCHAIN_PRECISION); + transfer_operation top; + top.from = alice_acc.id; + top.to = charlie_acc.id; + top.amount = transfer_amount; + + con_bob.wallet_api_ptr->add_operation_to_builder_transaction(tx_bob_handle, top); + asset transfer_fee = con_bob.wallet_api_ptr->set_fees_on_builder_transaction(tx_bob_handle, GRAPHENE_SYMBOL); + + // Sign the transaction with the explicit Bob key + signed_trx = con_bob.wallet_api_ptr->sign_builder_transaction2(tx_bob_handle, {bob_bki.pub_key}, true); + + // Check for one signatures on the transaction + BOOST_CHECK_EQUAL(signed_trx.signatures.size(), 1); + + // Check that the signed transaction contains Bob's signature + BOOST_CHECK_EQUAL(nathan_acct.active.get_keys().size(), 1); + expected_signers = {bob_bki.pub_key}; + actual_signers = con_bob.wallet_api_ptr->get_transaction_signers(signed_trx); + BOOST_CHECK(actual_signers == expected_signers); + + + ////// + // Check account balances + ////// + // Check Charlie's balances + vector charlie_balances = con.wallet_api_ptr->list_account_balances("charlie"); + BOOST_CHECK_EQUAL(charlie_balances.size(), 1); + asset charlie_core_balance = charlie_balances.front(); + asset expected_charlie_core_balance = transfer_amount; + BOOST_ASSERT(charlie_core_balance == expected_charlie_core_balance); + + // Check Bob's balances + vector bob_balances = con.wallet_api_ptr->list_account_balances("bob"); + BOOST_CHECK_EQUAL(bob_balances.size(), 0); + + // Check Alice's balance + vector alice_balances = con.wallet_api_ptr->list_account_balances("alice"); + BOOST_CHECK_EQUAL(alice_balances.size(), 1); + asset alice_core_balance = alice_balances.front(); + asset expected_alice_balance = asset(450000 * GRAPHENE_BLOCKCHAIN_PRECISION) + - expected_charlie_core_balance + - ca_fee - transfer_fee; + BOOST_CHECK(alice_core_balance.asset_id == expected_alice_balance.asset_id); + BOOST_CHECK_EQUAL(alice_core_balance.amount.value, expected_alice_balance.amount.value); + + } catch (fc::exception &e) { + edump((e.to_detail_string())); + throw; + } +}