From 71177fe9df48276e6b1c6ec5abf4a004218ed7f2 Mon Sep 17 00:00:00 2001 From: Sean Darcy Date: Thu, 13 Apr 2023 07:47:09 +1000 Subject: [PATCH] Wallet3 Unstake Tx --- .../cli-wallet/oxen_wallet_cli/walletcli.py | 13 ++++ src/wallet3/db/walletdb.cpp | 42 ++++++++++ src/wallet3/db/walletdb.hpp | 4 + src/wallet3/keyring.cpp | 16 ++++ src/wallet3/keyring.hpp | 3 + src/wallet3/rpc/command_parser.cpp | 6 +- src/wallet3/rpc/commands.h | 2 +- src/wallet3/rpc/request_handler.cpp | 24 +++++- src/wallet3/transaction_constructor.cpp | 76 +++++++++++++++++++ src/wallet3/transaction_constructor.hpp | 6 ++ 10 files changed, 189 insertions(+), 3 deletions(-) diff --git a/src/wallet3/cli-wallet/oxen_wallet_cli/walletcli.py b/src/wallet3/cli-wallet/oxen_wallet_cli/walletcli.py index 3662dd3b8..32d9c90f5 100644 --- a/src/wallet3/cli-wallet/oxen_wallet_cli/walletcli.py +++ b/src/wallet3/cli-wallet/oxen_wallet_cli/walletcli.py @@ -247,6 +247,19 @@ def stake(): stake_response = stake_future.get(); click.echo("Stake Response: {}".format(stake_response)) +@walletcli.command() +def unstake(): + service_node_key = click.prompt("Enter the public key of the service node you wish to unstake from: ", default="").strip() + if service_node_key == "": + click.prompt("Invalid public key entered") + return + unstake_params = { + "service_node_key": service_node_key, + } + stake_future = context.rpc_future("restricted.request_stake_unlock", args=unstake_params); + stake_response = stake_future.get(); + click.echo("Unstake Response: {}".format(stake_response)) + lokinet_years_dict = {"1": "lokinet", "2": "lokinet_2years", "5": "lokinet_5years", "10": "lokinet_10years"} # TODO better names for these ONS commands diff --git a/src/wallet3/db/walletdb.cpp b/src/wallet3/db/walletdb.cpp index f761592b2..db581d435 100644 --- a/src/wallet3/db/walletdb.cpp +++ b/src/wallet3/db/walletdb.cpp @@ -447,6 +447,48 @@ std::vector WalletDB::available_outputs(std::optional min_amoun return outs; } +Output WalletDB::get_output_from_key_image(const std::string& key_image) { + Output out; + + std::string query = + "SELECT amount, output_index, global_index, " + "unlock_time, block_height, output_key, derivation, rct_mask, key_images.key_image, " + "spent_height, spending FROM outputs JOIN key_images ON outputs.key_image = " + "key_images.id WHERE spent_height = 0 AND spending = FALSE AND key_image.key_image = ?"; + + auto st = prepared_st(query); + + st->bind(1, key_image); + + while (st->executeStep()) { + auto from_db = + db::get(st); + out.amount = std::get<0>(from_db); + out.output_index = std::get<1>(from_db); + out.global_index = std::get<2>(from_db); + out.unlock_time = std::get<3>(from_db); + out.block_height = std::get<4>(from_db); + tools::hex_to_type(std::get<5>(from_db), out.key); + tools::hex_to_type(std::get<6>(from_db), out.derivation); + tools::hex_to_type(std::get<7>(from_db), out.rct_mask); + tools::hex_to_type(std::get<8>(from_db), out.key_image); + out.spent_height = std::get<9>(from_db); + out.spending = std::get<10>(from_db); + } + + return out; +} + int64_t WalletDB::chain_output_count() { return get_metadata_int("output_count"); } diff --git a/src/wallet3/db/walletdb.hpp b/src/wallet3/db/walletdb.hpp index b85a8e2ce..c1e0cdbf2 100644 --- a/src/wallet3/db/walletdb.hpp +++ b/src/wallet3/db/walletdb.hpp @@ -93,6 +93,10 @@ class WalletDB : public db::Database { // TODO: subaddress specification std::vector available_outputs(std::optional min_amount); + // Finds a single output from our available spends with the provided key image + // used when unstaking a service node + Output get_output_from_key_image(const std::string& key_image); + // Gets the total number of outputs on the chain. Since all Oxen outputs are RingCT // and thus mixable, this can be used for decoy selection. int64_t chain_output_count(); diff --git a/src/wallet3/keyring.cpp b/src/wallet3/keyring.cpp index 3d1bc8245..483a7a968 100644 --- a/src/wallet3/keyring.cpp +++ b/src/wallet3/keyring.cpp @@ -495,4 +495,20 @@ ons::generic_signature Keyring::generate_ons_signature( return result; } +crypto::signature Keyring::generate_stake_unlock_signature(const Output& locked_stake_output) { + crypto::signature signature; + + // Calculate the outputs spending keypair + auto output_private_key = derive_output_secret_key(locked_stake_output.derivation, locked_stake_output.output_index, locked_stake_output.subaddress_index); + + crypto::public_key output_pubkey_computed; + key_device.secret_key_to_public_key(output_private_key, output_pubkey_computed); + + // Use the keypair for our signature to go into the txextra for stake unlock + if (!key_device.generate_unlock_signature(output_pubkey_computed, output_private_key, signature)) + throw std::runtime_error("Hardware device failed to sign the unlock request"); + + return signature; +} + } // namespace wallet diff --git a/src/wallet3/keyring.hpp b/src/wallet3/keyring.hpp index d37472c46..5e22545f0 100644 --- a/src/wallet3/keyring.hpp +++ b/src/wallet3/keyring.hpp @@ -116,6 +116,9 @@ class Keyring : public WalletKeys { const crypto::hash& prev_txid, const cryptonote::network_type& nettype); + virtual crypto::signature generate_stake_unlock_signature( + const Output& locked_stake_output); + cryptonote::network_type nettype; crypto::secret_key spend_private_key; diff --git a/src/wallet3/rpc/command_parser.cpp b/src/wallet3/rpc/command_parser.cpp index 9e438750c..931a758ee 100644 --- a/src/wallet3/rpc/command_parser.cpp +++ b/src/wallet3/rpc/command_parser.cpp @@ -282,7 +282,11 @@ void parse_request(REGISTER_SERVICE_NODE& req, rpc_input in) { ); } -void parse_request(REQUEST_STAKE_UNLOCK& req, rpc_input in) {} +void parse_request(REQUEST_STAKE_UNLOCK& req, rpc_input in) { + get_values(in, + "service_node_key", req.request.service_node_key + ); +} void parse_request(CAN_REQUEST_STAKE_UNLOCK& req, rpc_input in) {} diff --git a/src/wallet3/rpc/commands.h b/src/wallet3/rpc/commands.h index 261a44e1f..f5ce70293 100644 --- a/src/wallet3/rpc/commands.h +++ b/src/wallet3/rpc/commands.h @@ -2257,7 +2257,7 @@ struct REQUEST_STAKE_UNLOCK : RESTRICTED { struct REQUEST { std::string service_node_key; // Service Node Public Key. - }; + } request; }; /// Check if Service Node can unlock its stake. diff --git a/src/wallet3/rpc/request_handler.cpp b/src/wallet3/rpc/request_handler.cpp index be54adfa0..2f5cc6c63 100644 --- a/src/wallet3/rpc/request_handler.cpp +++ b/src/wallet3/rpc/request_handler.cpp @@ -397,7 +397,29 @@ void RequestHandler::invoke(REGISTER_SERVICE_NODE& command, rpc_context context) command.response["status"] = "200"; } -void RequestHandler::invoke(REQUEST_STAKE_UNLOCK& command, rpc_context context) {} +void RequestHandler::invoke(REQUEST_STAKE_UNLOCK& command, rpc_context context) { + oxen::log::info(logcat, "RPC Handler received REQUEST_STAKE_UNLOCK command"); + wallet::PendingTransaction ptx; + if (auto w = wallet.lock()) + { + cryptonote::tx_destination_entry change_dest; + change_dest.original = w->keys->get_main_address(); + cryptonote::address_parse_info change_addr_info; + cryptonote::get_account_address_from_str(change_addr_info, w->nettype, change_dest.original); + change_dest.amount = 0; + change_dest.addr = change_addr_info.address; + change_dest.is_subaddress = change_addr_info.is_subaddress; + change_dest.is_integrated = change_addr_info.has_payment_id; + + ptx = w->tx_constructor->create_stake_unlock_transaction( + command.request.service_node_key, + change_dest, + w->keys + ); + } + command.response["result"] = submit_transaction(ptx); + command.response["status"] = "200"; +} void RequestHandler::invoke(CAN_REQUEST_STAKE_UNLOCK& command, rpc_context context) {} diff --git a/src/wallet3/transaction_constructor.cpp b/src/wallet3/transaction_constructor.cpp index 4a185cfd9..f552d8b5e 100644 --- a/src/wallet3/transaction_constructor.cpp +++ b/src/wallet3/transaction_constructor.cpp @@ -442,6 +442,82 @@ PendingTransaction TransactionConstructor::create_ons_update_transaction( return new_tx; } + PendingTransaction + TransactionConstructor::create_stake_unlock_transaction( + const std::string& service_node_key, + const cryptonote::tx_destination_entry& change_recipient, + std::shared_ptr keyring) + { + + std::vector recipients; + PendingTransaction new_tx(recipients); + auto [hf, hf_uint8] = cryptonote::get_ideal_block_version(db->network_type(), db->scan_target_height()); + new_tx.tx.version = cryptonote::transaction::get_max_version_for_hf(hf); + new_tx.tx.type = cryptonote::txtype::stake; + new_tx.fee_per_byte = fee_per_byte; + new_tx.fee_per_output = fee_per_output; + new_tx.change = change_recipient; + new_tx.blink = false; + + crypto::public_key service_node_public_key; + if (!tools::hex_to_type(service_node_key, service_node_public_key)) + throw std::runtime_error("could not read service node key"); + cryptonote::add_service_node_pubkey_to_tx_extra(new_tx.extra, service_node_public_key); + + auto get_service_node_future = daemon->get_service_nodes({service_node_key}); + if (get_service_node_future.wait_for(5s) != std::future_status::ready) + throw std::runtime_error("request to daemon for get_service_nodes timed out"); + + auto response = get_service_node_future.get(); + if(response.is_finished()) + throw std::runtime_error("Could not find service node in service node list, please make sure it is registered first."); + auto snode_info = response.consume_dict_consumer(); + + const auto hf_version = cryptonote::get_latest_hard_fork(nettype).version; + + if (not snode_info.skip_until("contributors")) + throw std::runtime_error{"Invalid response from daemon"}; + auto contributors = snode_info.consume_list_consumer(); + + cryptonote::tx_extra_tx_key_image_unlock unlock = {}; + unlock.nonce = cryptonote::tx_extra_tx_key_image_unlock::FAKE_NONCE; + // Loop over contributors + bool found_our_contribution = false; + while (not contributors.is_finished()) + { + auto contributor = contributors.consume_dict_consumer(); + + if (not contributor.skip_until("address")) + throw std::runtime_error{"Invalid response from daemon"}; + auto contributor_address = contributor.consume_string(); + if (contributor_address != change_recipient.address(nettype, {})) + continue; + + found_our_contribution = true; + + if (not contributor.skip_until("key_image")) + throw std::runtime_error{"Invalid response from daemon"}; + + const auto key_image = response.consume_string(); + if(!tools::hex_to_type(key_image, unlock.key_image)) + throw std::runtime_error{"Failed to parse hex representation of key image"s + key_image}; + + const auto locked_stake_output = db->get_output_from_key_image(key_image); + + unlock.signature = keyring->generate_stake_unlock_signature(locked_stake_output); + } + + // If did not find then throw + if (not found_our_contribution) + throw std::runtime_error{"did not find our contribution in this service node"}; + + add_tx_key_image_unlock_to_tx_extra(new_tx.extra, unlock); + new_tx.update_change(); + select_inputs_and_finalise(new_tx); + + return new_tx; + } + // SelectInputs will choose some available unspent outputs from the database and allocate to the // transaction can be called multiple times and will add until enough is sufficient diff --git a/src/wallet3/transaction_constructor.hpp b/src/wallet3/transaction_constructor.hpp index 87e03d8e1..47d64e0c5 100644 --- a/src/wallet3/transaction_constructor.hpp +++ b/src/wallet3/transaction_constructor.hpp @@ -92,6 +92,12 @@ class TransactionConstructor { const cryptonote::tx_destination_entry& change_recipient ); + PendingTransaction + create_stake_unlock_transaction( + const std::string& service_node_key, + const cryptonote::tx_destination_entry& change_recipient, + std::shared_ptr keyring); + uint64_t fee_per_byte = cryptonote::FEE_PER_BYTE_V13; uint64_t fee_per_output = cryptonote::FEE_PER_OUTPUT_V18;