Wallet3 Unstake Tx

This commit is contained in:
Sean Darcy 2023-04-13 07:47:09 +10:00
parent d9b5eb5e12
commit 71177fe9df
10 changed files with 189 additions and 3 deletions

View File

@ -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

View File

@ -447,6 +447,48 @@ std::vector<Output> WalletDB::available_outputs(std::optional<int64_t> 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<int64_t,
int64_t,
int64_t,
int64_t,
int64_t,
std::string,
std::string,
std::string,
std::string,
int64_t,
int64_t>(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");
}

View File

@ -93,6 +93,10 @@ class WalletDB : public db::Database {
// TODO: subaddress specification
std::vector<Output> available_outputs(std::optional<int64_t> 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();

View File

@ -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

View File

@ -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;

View File

@ -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) {}

View File

@ -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.

View File

@ -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) {}

View File

@ -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> keyring)
{
std::vector<cryptonote::tx_destination_entry> 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

View File

@ -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> keyring);
uint64_t fee_per_byte = cryptonote::FEE_PER_BYTE_V13;
uint64_t fee_per_output = cryptonote::FEE_PER_OUTPUT_V18;