This commit is contained in:
Sean 2023-05-27 21:42:23 +01:00 committed by GitHub
commit 71d2be3b44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 890 additions and 90 deletions

View File

@ -80,10 +80,14 @@ def load_test_wallet():
click.echo("Wallet already loaded")
return
spend_priv = "e6c9165356c619a64a0d26fafd99891acccccf8717a8067859d972ecd8bcfc0a"
spend_pub = "b76f2d7c8a036ff65c564dcb27081c04fe3f2157942e23b0496ca797ba728e4f"
view_priv = "961d67bb5b3ed1af8678bbfcf621f9c15c2b7bff080892890020bdfd47fe4f0a"
view_pub = "8a0ebacd613e0b03b8f27bc64bd961ea2ebf4c671c6e7f3268651acf0823fed5"
# spend_priv = "e6c9165356c619a64a0d26fafd99891acccccf8717a8067859d972ecd8bcfc0a"
# spend_pub = "b76f2d7c8a036ff65c564dcb27081c04fe3f2157942e23b0496ca797ba728e4f"
# view_priv = "961d67bb5b3ed1af8678bbfcf621f9c15c2b7bff080892890020bdfd47fe4f0a"
# view_pub = "8a0ebacd613e0b03b8f27bc64bd961ea2ebf4c671c6e7f3268651acf0823fed5"
view_pub = "ed26f4f9ed44baccb0aa32bfd91fd546115a60c77e6e8098cd4debf8f33cb9f9"
spend_pub = "9834c238ebecb78b1f30115c50b956e9e5e0d86072c61d57e65ee04f9c650b40"
view_priv = "5f51194e0f839ee32fdd85765be009b1fceb70e78204e4bfa3010e2ade61fc0d"
spend_priv = "0ac3dc5fff3a7a303b893a50119ff2da3125f6a51b980e409d6c8a3a3f7ec80b"
keyring = pywallet3.Keyring(spend_priv, spend_pub, view_priv, view_pub, context.options["network"])
click.echo("Wallet address " + click.style("{}", fg='cyan', bold=True).format(keyring.get_main_address()) + " loaded")
@ -195,13 +199,71 @@ def transfer():
if address == "" or amount == 0.0:
click.prompt("Invalid address/amount entered")
return
amount_in_atomic_units = round(amount * OXEN_ATOMIC_UNITS, 0);
amount_in_atomic_units = round(amount * OXEN_ATOMIC_UNITS);
destination = {"address": address, "amount": amount_in_atomic_units}
transfer_params = {"destinations": [destination]}
transfer_future = context.rpc_future("restricted.transfer", args=transfer_params);
transfer_response = transfer_future.get();
click.echo("Transfer Response: {}".format(transfer_response))
@walletcli.command(name='register_service_node')
@click.argument('fee', nargs=1, type=click.INT)
@click.argument('address_amount', nargs=-1)
@click.argument('hardfork', nargs=1, type=click.INT)
@click.argument('service_node_pubkey', nargs=1)
@click.argument('signature', nargs=1)
def register_service_node(fee, address_amount, hardfork, service_node_pubkey, signature):
addresses = address_amount[::2]
amounts = [int(x) for x in address_amount[1::2]]
print(fee)
register_params= {
"fee": fee,
"addresses": addresses,
"amounts": amounts,
"hardfork": hardfork,
"service_node_key": service_node_pubkey,
"signature": signature
}
register_future = context.rpc_future("restricted.register_service_node", args=register_params);
register_response = register_future.get();
click.echo("Register Service Node Response: {}".format(register_response))
@walletcli.command()
def stake():
service_node_key = click.prompt("Enter the public key of the service node you wish to stake to: ", default="").strip()
amount = click.prompt("Enter the amount in oxen to be contributed to (Optional: 0 will automatically contribute the minimum){}".format(service_node_key), default=0.0)
if service_node_key == "":
click.prompt("Invalid public key entered")
return
amount_in_atomic_units = round(amount * OXEN_ATOMIC_UNITS);
get_address_future = context.rpc_future("rpc.get_address");
get_address_response = get_address_future.get();
stake_params = {
"destination": get_address_response['address'],
"service_node_key": service_node_key,
"amount": amount_in_atomic_units
}
stake_future = context.rpc_future("restricted.stake", args=stake_params);
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
@ -241,13 +303,13 @@ def ons_update_mapping():
}
ons_value = click.prompt("Optional: Please enter a value to modify the ons mapping", default="").strip()
if len(ons_value) > 0:
ons_buy_params["value"] = ons_value
ons_update_params["value"] = ons_value
ons_owner = click.prompt("Optional: Please enter an address to modify the owner", default="").strip()
if len(ons_owner) > 0:
ons_buy_params["owner"] = ons_owner
ons_update_params["owner"] = ons_owner
ons_backup_owner = click.prompt("Optional: Please enter an address to modify the backup owner", default="").strip()
if len(ons_backup_owner) > 0:
ons_buy_params["backup_owner"] = ons_backup_owner
ons_update_params["backup_owner"] = ons_backup_owner
transfer_future = context.rpc_future("restricted.ons_update_mapping", args=ons_update_params);
transfer_response = transfer_future.get();

View File

@ -52,8 +52,11 @@ class DaemonComms {
virtual std::future<std::string> submit_transaction(
const cryptonote::transaction& tx, bool blink) = 0;
virtual std::future<std::pair<std::string, crypto::hash>> ons_names_to_owners(
const std::string& name_hash, uint16_t type) = 0;
};
virtual std::future<std::pair<std::string, crypto::hash>>
ons_names_to_owners(const std::string& name_hash, uint16_t type) = 0;
virtual std::future<oxenc::bt_list_consumer>
get_service_nodes(const std::vector<std::string>& service_node_keys) = 0;
};
} // namespace wallet

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

@ -476,8 +476,42 @@ std::future<std::pair<std::string, crypto::hash>> DefaultDaemonComms::ons_names_
return fut;
}
void DefaultDaemonComms::register_wallet(
wallet::Wallet& wallet, int64_t height, bool check_sync_height, bool new_wallet) {
std::future<oxenc::bt_list_consumer>
DefaultDaemonComms::get_service_nodes(const std::vector<std::string>& service_node_keys)
{
auto p = std::make_shared<std::promise<oxenc::bt_list_consumer>>();
auto fut = p->get_future();
auto req_cb = [p=std::move(p)](bool ok, std::vector<std::string> response)
{
try
{
oxenc::bt_dict_consumer dc{response[1]};
if (not dc.skip_until("service_node_states"))
throw std::runtime_error{"Invalid response from daemon"};
auto result_list = dc.consume_list_consumer();
p->set_value(result_list);
} catch (...) {
p->set_exception(std::current_exception());
}
};
oxenc::bt_list key_list_bt;
std::copy( service_node_keys.begin(), service_node_keys.end(), std::back_inserter( key_list_bt) );
oxenc::bt_dict req_params_dict{
{"service_node_pubkeys", key_list_bt}
};
omq->request(conn, "rpc.get_service_nodes", req_cb, oxenc::bt_serialize(req_params_dict));
return fut;
}
void
DefaultDaemonComms::register_wallet(wallet::Wallet& wallet, int64_t height, bool check_sync_height, bool new_wallet)
{
oxen::log::trace(logcat, "Daemon Comms register_wallet called");
omq->job(
[this, w = wallet.shared_from_this(), height, check_sync_height, new_wallet]() {

View File

@ -49,7 +49,11 @@ class DefaultDaemonComms : public DaemonComms,
std::future<std::pair<std::string, crypto::hash>> ons_names_to_owners(
const std::string& name_hash, const uint16_t type);
private:
std::future<oxenc::bt_list_consumer>
get_service_nodes(const std::vector<std::string>& service_node_keys);
private:
void for_each_wallet(std::function<void(std::shared_ptr<Wallet>)> func);
void get_blocks();

View File

@ -101,6 +101,22 @@ crypto::key_image Keyring::key_image(
return ret;
}
crypto::key_image Keyring::generate_key_image(const crypto::secret_key& output_private_key) {
crypto::key_image ret;
crypto::public_key output_pubkey_computed;
key_device.secret_key_to_public_key(output_private_key, output_pubkey_computed);
key_device.generate_key_image(output_pubkey_computed, output_private_key, ret);
return ret;
}
crypto::signature Keyring::generate_key_image_signature(const crypto::secret_key& output_private_key, const crypto::key_image& key_image) {
crypto::signature ret;
crypto::public_key output_pubkey_computed;
key_device.secret_key_to_public_key(output_private_key, output_pubkey_computed);
key_device.generate_key_image_signature(key_image, output_pubkey_computed, output_private_key, ret);
return ret;
}
// TODO: replace later when removing wallet2½ layer
std::pair<uint64_t, rct::key> Keyring::output_amount_and_mask(
const rct::rctSig& rv, const crypto::key_derivation& derivation, unsigned int i) {
@ -213,7 +229,7 @@ crypto::hash Keyring::get_transaction_prefix_hash(const cryptonote::transaction_
void Keyring::sign_transaction(PendingTransaction& ptx) {
auto hf_version = cryptonote::hf::hf19_reward_batching;
auto tx_key = generate_tx_key(hf_version);
auto tx_key = ptx.tx_secret_key.value_or(generate_tx_key(hf_version));
rct::ctkeyV inSk;
rct::keyV dest_keys;
@ -495,4 +511,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

@ -77,6 +77,10 @@ class Keyring : public WalletKeys {
uint64_t output_index,
const cryptonote::subaddress_index& sub_index);
virtual crypto::key_image generate_key_image(const crypto::secret_key& output_private_key);
virtual crypto::signature generate_key_image_signature(const crypto::secret_key& output_private_key, const crypto::key_image& key_image);
virtual std::pair<uint64_t, rct::key> output_amount_and_mask(
const rct::rctSig& rv, const crypto::key_derivation& derivation, unsigned int i);
@ -116,6 +120,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

@ -45,7 +45,6 @@ int64_t PendingTransaction::get_fee() const {
}
int64_t PendingTransaction::get_fee(int64_t n_inputs) const {
int64_t fixed_fee = burn_fixed;
// TODO sean add this
int64_t burn_pct = 0;
int64_t fee_percent = oxen::BLINK_BURN_TX_FEE_PERCENT_V18; // 100%
if (blink)

View File

@ -37,6 +37,8 @@ struct PendingTransaction {
uint64_t burn_fixed = 0;
std::optional<crypto::secret_key> tx_secret_key = std::nullopt;
std::vector<uint8_t> extra = {};
size_t extra_size() const { return extra.size(); };

View File

@ -253,11 +253,40 @@ void parse_request(SUBMIT_MULTISIG& req, rpc_input in) {}
void parse_request(GET_VERSION& req, rpc_input in) {}
void parse_request(STAKE& req, rpc_input in) {}
void parse_request(STAKE& req, rpc_input in) {
get_values(in,
"amount", req.request.amount,
"destination", req.request.destination,
"do_not_relay", req.request.do_not_relay,
"get_tx_hex", req.request.get_tx_hex,
"get_tx_key", req.request.get_tx_key,
"get_tx_metadata", req.request.get_tx_metadata,
"priority", req.request.priority,
"service_node_key", req.request.service_node_key,
"subaddr_indices", req.request.subaddr_indices
);
}
void parse_request(REGISTER_SERVICE_NODE& req, rpc_input in) {}
void parse_request(REGISTER_SERVICE_NODE& req, rpc_input in) {
get_values(in,
"addresses", req.request.addresses,
"amounts", req.request.amounts,
"do_not_relay", req.request.do_not_relay,
"fee", req.request.fee,
"get_tx_hex", req.request.get_tx_hex,
"get_tx_key", req.request.get_tx_key,
"get_tx_metadata", req.request.get_tx_metadata,
"hardfork", req.request.hardfork,
"service_node_key", req.request.service_node_key,
"signature", req.request.signature
);
}
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

@ -2182,58 +2182,63 @@ struct GET_VERSION : RPC_COMMAND {
struct STAKE : RESTRICTED {
static constexpr auto names() { return NAMES("stake"); }
struct REQUEST {
std::string destination; // Primary Public address that the rewards will go to.
uint64_t amount; // Amount of Loki to stake in atomic units.
std::set<uint32_t> subaddr_indices; // (Optional) Transfer from this set of subaddresses.
// (Defaults to 0)
std::string service_node_key; // Service Node Public Address.
uint32_t priority; // Set a priority for the transaction. Accepted values are: or 0-4 for:
// default, unimportant, normal, elevated, priority.
bool get_tx_key; // (Optional) Return the transaction key after sending.
bool do_not_relay; // (Optional) If true, the newly created transaction will not be relayed
// to the oxen network. (Defaults to false)
bool get_tx_hex; // Return the transaction as hex string after sending (Defaults to false)
bool get_tx_metadata; // Return the metadata needed to relay the transaction. (Defaults to
// false)
struct REQUEST
{
std::string destination; // Primary Public address that the rewards will go to.
uint64_t amount; // Amount of Loki to stake in atomic units.
std::vector<uint32_t> subaddr_indices; // (Optional) Transfer from this set of subaddresses. (Defaults to 0)
std::string service_node_key; // Service Node Public Address.
uint32_t priority; // Set a priority for the transaction. Accepted values are: or 0-4 for: default, unimportant, normal, elevated, priority.
bool get_tx_key; // (Optional) Return the transaction key after sending.
bool do_not_relay; // (Optional) If true, the newly created transaction will not be relayed to the oxen network. (Defaults to false)
bool get_tx_hex; // Return the transaction as hex string after sending (Defaults to false)
bool get_tx_metadata; // Return the metadata needed to relay the transaction. (Defaults to false)
} request;
};
/// Register Service Node.
///
/// Inputs:
///
/// - \p register_service_node_str -- String supplied by the prepare_registration command.
/// - \p get_tx_key -- (Optional) Return the transaction key after sending.
/// - \p do_not_relay -- (Optional) If true, the newly created transaction will not be relayed to
/// the oxen network. (Defaults to false)
/// - \p get_tx_hex -- Return the transaction as hex string after sending (Defaults to false)
/// - \p get_tx_metadata -- Return the metadata needed to relay the transaction. (Defaults to false)
///
/// Outputs:
///
/// - \p tx_hash -- Publicly searchable transaction hash.
/// - \p tx_key -- Transaction key if get_tx_key is true, otherwise, blank string.
/// - \p amount -- Amount transferred for the transaction in atomic units.
/// - \p fee -- Value in atomic units of the fee charged for the tx.
/// - \p tx_blob -- Raw transaction represented as hex string, if get_tx_hex is true.
/// - \p tx_metadata -- Set of transaction metadata needed to relay this transfer later, if
/// `get_tx_metadata` is `true`.
/// - \p multisig_txset -- Set of multisig transactions in the process of being signed (empty for
/// non-multisig).
/// - \p unsigned_txset -- Set of unsigned tx for cold-signing purposes.
struct REGISTER_SERVICE_NODE : RESTRICTED {
OXEN_RPC_DOC_INTROSPECT
/// Register Service Node.
///
/// Inputs:
///
/// - \p fee_basis_points --
/// - \p addresses --
/// - \p amounts --
/// - \p fee --
/// - \p timestamp --
/// - \p service_node_key --
/// - \p signature --
/// - \p get_tx_key -- (Optional) Return the transaction key after sending.
/// - \p do_not_relay -- (Optional) If true, the newly created transaction will not be relayed to the oxen network. (Defaults to false)
/// - \p get_tx_hex -- Return the transaction as hex string after sending (Defaults to false)
/// - \p get_tx_metadata -- Return the metadata needed to relay the transaction. (Defaults to false)
///
/// Outputs:
///
/// - \p tx_hash -- Publicly searchable transaction hash.
/// - \p tx_key -- Transaction key if get_tx_key is true, otherwise, blank string.
/// - \p amount -- Amount transferred for the transaction in atomic units.
/// - \p fee -- Value in atomic units of the fee charged for the tx.
/// - \p tx_blob -- Raw transaction represented as hex string, if get_tx_hex is true.
/// - \p tx_metadata -- Set of transaction metadata needed to relay this transfer later, if `get_tx_metadata` is `true`.
/// - \p multisig_txset -- Set of multisig transactions in the process of being signed (empty for non-multisig).
/// - \p unsigned_txset -- Set of unsigned tx for cold-signing purposes.
struct REGISTER_SERVICE_NODE : RESTRICTED
{
static constexpr auto names() { return NAMES("register_service_node"); }
struct REQUEST {
std::string
register_service_node_str; // String supplied by the prepare_registration command.
bool get_tx_key; // (Optional) Return the transaction key after sending.
bool do_not_relay; // (Optional) If true, the newly created transaction will not be relayed
// to the oxen network. (Defaults to false)
bool get_tx_hex; // Return the transaction as hex string after sending (Defaults to false)
bool get_tx_metadata; // Return the metadata needed to relay the transaction. (Defaults to
// false)
struct REQUEST
{
uint64_t fee;
std::vector<std::string> addresses;
std::vector<uint64_t> amounts;
uint64_t hardfork;
std::string service_node_key;
std::string signature;
bool get_tx_key; // (Optional) Return the transaction key after sending.
bool do_not_relay; // (Optional) If true, the newly created transaction will not be relayed to the oxen network. (Defaults to false)
bool get_tx_hex; // Return the transaction as hex string after sending (Defaults to false)
bool get_tx_metadata; // Return the metadata needed to relay the transaction. (Defaults to false)
} request;
};
@ -2252,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

@ -56,17 +56,18 @@ void RequestHandler::set_wallet(std::weak_ptr<wallet::Wallet> ptr) {
wallet = ptr;
}
// TODO sean something here
std::string RequestHandler::submit_transaction(wallet::PendingTransaction& ptx) {
std::string response;
if (auto w = wallet.lock()) {
w->keys->sign_transaction(ptx);
std::string RequestHandler::submit_transaction(wallet::PendingTransaction& ptx)
{
std::string response;
if (auto w = wallet.lock())
{
w->keys->sign_transaction(ptx);
auto submit_future = w->daemon_comms->submit_transaction(ptx.tx, false);
auto submit_future = w->daemon_comms->submit_transaction(ptx.tx, false);
if (submit_future.wait_for(5s) != std::future_status::ready)
throw rpc_error(500, "request to daemon timed out");
response = submit_future.get();
if (submit_future.wait_for(5s) != std::future_status::ready)
throw rpc_error(500, "request to daemon timed out");
response = submit_future.get();
}
return response;
}
@ -343,11 +344,83 @@ void RequestHandler::invoke(GET_VERSION& command, rpc_context context) {
}
}
void RequestHandler::invoke(STAKE& command, rpc_context context) {}
void RequestHandler::invoke(STAKE& command, rpc_context context) {
oxen::log::info(logcat, "RPC Handler received STAKE 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;
void RequestHandler::invoke(REGISTER_SERVICE_NODE& command, rpc_context context) {}
ptx = w->tx_constructor->create_stake_transaction(
command.request.destination,
command.request.service_node_key,
command.request.amount,
change_dest
);
}
command.response["result"] = submit_transaction(ptx);
command.response["status"] = "200";
}
void RequestHandler::invoke(REQUEST_STAKE_UNLOCK& command, rpc_context context) {}
void RequestHandler::invoke(REGISTER_SERVICE_NODE& command, rpc_context context) {
oxen::log::info(logcat, "RPC Handler received REGISTER SERVICE NODE 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_register_service_node_transaction(
command.request.fee,
command.request.addresses,
command.request.amounts,
command.request.hardfork,
command.request.service_node_key,
command.request.signature,
change_dest,
w->keys
);
}
command.response["result"] = submit_transaction(ptx);
command.response["status"] = "200";
}
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

@ -9,13 +9,16 @@
#include "output_selection/output_selection.hpp"
#include "pending_transaction.hpp"
// TODO: nettype-based tx construction parameters
namespace wallet
{
static auto logcat = oxen::log::Cat("wallet");
namespace wallet {
// create_transaction will create a vanilla spend transaction without any special features.
PendingTransaction TransactionConstructor::create_transaction(
const std::vector<cryptonote::tx_destination_entry>& recipients,
const cryptonote::tx_destination_entry& change_recipient) {
// create_transaction will create a vanilla spend transaction without any special features.
PendingTransaction
TransactionConstructor::create_transaction(
const std::vector<cryptonote::tx_destination_entry>& recipients,
const cryptonote::tx_destination_entry& change_recipient)
{
PendingTransaction new_tx(recipients);
auto [hf, hf_uint8] =
cryptonote::get_ideal_block_version(db->network_type(), db->scan_target_height());
@ -127,7 +130,6 @@ PendingTransaction TransactionConstructor::create_ons_update_transaction(
if (submit_ons_future.wait_for(5s) != std::future_status::ready)
throw std::runtime_error("request to daemon for ons_names_to_owners timed out");
// TODO sean stuff goes here
const auto [curr_owner, prev_txid] = submit_ons_future.get();
ons::mapping_value encrypted_value;
@ -185,9 +187,360 @@ PendingTransaction TransactionConstructor::create_ons_update_transaction(
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
void TransactionConstructor::select_inputs(PendingTransaction& ptx) const {
void
TransactionConstructor::validate_register_service_node_parameters(
const std::string& service_node_key,
const service_nodes::registration_details& registration,
cryptonote::hf hf_version
)
{
auto staking_requirement = service_nodes::get_staking_requirement(nettype, db->scan_target_height());
auto now = std::chrono::system_clock::now();
if (uint64_t(hf_version) != registration.hf)
throw service_nodes::invalid_registration{"hardfork is invalid"};
// Validate registration
service_nodes::validate_registration(hf_version, nettype, staking_requirement, std::chrono::system_clock::to_time_t(now), registration);
auto hash = service_nodes::get_registration_hash(registration);
if (!crypto::check_key(registration.service_node_pubkey))
throw service_nodes::invalid_registration{"Service Node Key is not a valid public key (" + tools::type_to_hex(registration.service_node_pubkey) + ")"};
if (!crypto::check_signature(hash, registration.service_node_pubkey, registration.signature))
throw service_nodes::invalid_registration{"Registration signature verification failed for pubkey/hash: " +
tools::type_to_hex(registration.service_node_pubkey) + "/" + tools::type_to_hex(hash)};
// Check Service Node is able to be registered
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 service_nodes::invalid_registration{"This service node is already registered"};
}
PendingTransaction
TransactionConstructor::create_register_service_node_transaction(
const uint64_t fee,
const std::vector<std::string>& addresses,
const std::vector<uint64_t>& amounts,
const uint64_t registration_hardfork,
const std::string& service_node_key,
const std::string& signature_str,
const cryptonote::tx_destination_entry& change_recipient,
std::shared_ptr<Keyring> keyring)
{
std::vector<cryptonote::tx_destination_entry> recipients;
auto& staked_amount_to_self = recipients.emplace_back();
staked_amount_to_self.original = change_recipient.original;
staked_amount_to_self.amount = amounts[0];
staked_amount_to_self.addr = change_recipient.addr;
staked_amount_to_self.is_subaddress = change_recipient.is_subaddress;
staked_amount_to_self.is_integrated = change_recipient.is_integrated;
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;
cryptonote::add_service_node_contributor_to_tx_extra(new_tx.extra, change_recipient.addr);
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);
crypto::signature signature;
if (!tools::hex_to_type(signature_str, signature))
throw std::runtime_error("could not read signature");
service_nodes::registration_details registration{service_node_public_key, {}, fee, registration_hardfork, false, signature};
cryptonote::address_parse_info addr_info;
for (size_t i = 0; i < amounts.size(); i++) {
cryptonote::get_account_address_from_str(addr_info, nettype, addresses[i]);
if (addr_info.has_payment_id)
throw service_nodes::invalid_registration{"Can't use a payment id for staking tx"};
if (addr_info.is_subaddress)
throw service_nodes::invalid_registration{"Can't use a subaddress for staking tx"};
registration.reserved.emplace_back(addr_info.address, amounts[i]);
}
if (!cryptonote::add_service_node_registration_to_tx_extra(new_tx.extra, registration))
throw std::runtime_error("Failed to serialize service node registration tx extra");
new_tx.tx_secret_key = keyring->generate_tx_key(hf);
cryptonote::add_tx_secret_key_to_tx_extra(new_tx.extra, *new_tx.tx_secret_key);
// TODO this sends the secret key to the hardware device so it know to use it
//if (!hwdev.update_staking_tx_secret_key(tx_sk)) {
//log::warning(globallogcat, "Failed to add tx secret key to stake transaction");
//return false;
//}
cryptonote::tx_extra_tx_key_image_proofs key_image_proofs;
auto& proof = key_image_proofs.proofs.emplace_back();
proof.key_image = keyring->generate_key_image(*new_tx.tx_secret_key);
proof.signature = keyring->generate_key_image_signature(*new_tx.tx_secret_key, proof.key_image);
cryptonote::add_tx_key_image_proofs_to_tx_extra(new_tx.extra, key_image_proofs);
validate_register_service_node_parameters(service_node_key, registration, hf);
new_tx.update_change();
//TODO sean get use the new tx_key from somewhere
select_inputs_and_finalise(new_tx);
return new_tx;
}
void
TransactionConstructor::validate_stake_parameters(
const std::string& service_node_key,
uint64_t& amount,
const cryptonote::tx_destination_entry& change_recipient
)
{
if (change_recipient.is_integrated)
throw std::runtime_error{"Payment IDs cannot be used in a staking transaction"};
if (change_recipient.is_subaddress)
throw std::runtime_error{"Subaddresses cannot be used in a staking transaction"};
/// check that the service node is registered
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();
if (not snode_info.skip_until("staking_requirement"))
throw std::runtime_error{"Invalid response from daemon"};
const auto staking_req = snode_info.consume_integer<int64_t>();
if (not snode_info.skip_until("total_contributed"))
throw std::runtime_error{"Invalid response from daemon"};
const auto total_contributed = snode_info.consume_integer<int64_t>();
uint64_t total_res = 0;
if (snode_info.skip_until("total_reserved"))
total_res = snode_info.consume_integer<int64_t>();
size_t total_existing_contributions = 0; // Count both contributions and reserved spots
bool is_preexisting_contributor = false;
uint64_t reserved_amount_not_contributed_yet = 0;
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 (not contributor.skip_until("amount"))
throw std::runtime_error{"Invalid response from daemon"};
auto amount = contributor.consume_integer<int64_t>();
if (not contributor.skip_until("locked_contributions"))
throw std::runtime_error{"Invalid response from daemon"};
auto locked_contributions = contributor.consume_list_consumer();
while (not locked_contributions.is_finished())
{
locked_contributions.consume_dict_consumer();
total_existing_contributions++;
}
int64_t reserved = 0;
if (contributor.skip_until("reserved"))
reserved = contributor.consume_integer<int64_t>();
if (reserved > amount)
total_existing_contributions++; // reserved contributor spot
if (contributor_address == change_recipient.address(nettype, {}))
{
is_preexisting_contributor = true;
reserved_amount_not_contributed_yet = reserved - amount;
}
}
uint64_t max_contrib_total = staking_req - total_res + reserved_amount_not_contributed_yet;
uint64_t min_contrib_total = service_nodes::get_min_node_contribution(hf_version, staking_req, total_res, total_existing_contributions);
if (min_contrib_total == UINT64_MAX || reserved_amount_not_contributed_yet > min_contrib_total)
min_contrib_total = reserved_amount_not_contributed_yet;
if (max_contrib_total == 0)
throw std::runtime_error("The service node cannot receive any more Oxen from this wallet");
const bool full = total_existing_contributions >= oxen::MAX_CONTRIBUTORS_HF19;
if (full && !is_preexisting_contributor)
throw std::runtime_error("The service node already has the maximum number of participants and this wallet is not one of them");
if (amount == 0)
{
oxen::log::info(logcat, "No amount provided to stake txn, assuming minimum contributrion of: {}", cryptonote::print_money(min_contrib_total));
amount = min_contrib_total;
}
if (amount < min_contrib_total)
{
const uint64_t DUST = oxen::MAX_CONTRIBUTORS_HF19;
if (min_contrib_total - amount <= DUST)
{
oxen::log::info(logcat, "Seeing as this is insufficient by dust amounts, amount was increased automatically to ", cryptonote::print_money(min_contrib_total));
amount = min_contrib_total;
}
else
throw std::runtime_error(fmt::format("You must contribute at least {} oxen to become a contributor for this service node.", min_contrib_total));
}
if (amount > max_contrib_total)
{
oxen::log::info(logcat, "You may only contribute up to {} more oxen to this service node. Reducing your stake from {} to {}", max_contrib_total, amount, max_contrib_total);
amount = max_contrib_total;
}
}
PendingTransaction
TransactionConstructor::create_stake_transaction(
const std::string& destination,
const std::string& service_node_key,
const uint64_t requested_amount,
const cryptonote::tx_destination_entry& change_recipient
)
{
uint64_t amount = requested_amount;
double amount_fraction = 0;
validate_stake_parameters(service_node_key, amount, change_recipient);
std::vector<cryptonote::tx_destination_entry> recipients;
auto& staked_amount_to_self = recipients.emplace_back();
staked_amount_to_self.original = change_recipient.original;
staked_amount_to_self.amount = amount;
staked_amount_to_self.addr = change_recipient.addr;
staked_amount_to_self.is_subaddress = change_recipient.is_subaddress;
staked_amount_to_self.is_integrated = change_recipient.is_integrated;
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);
cryptonote::add_service_node_contributor_to_tx_extra(new_tx.extra, change_recipient.addr);
new_tx.update_change();
select_inputs_and_finalise(new_tx);
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
void
TransactionConstructor::select_inputs(PendingTransaction& ptx) const
{
const int64_t single_input_size = ptx.get_fee(1);
const int64_t double_input_size = ptx.get_fee(2);
const int64_t additional_input = double_input_size - single_input_size;

View File

@ -59,6 +59,45 @@ class TransactionConstructor {
const cryptonote::tx_destination_entry& change_recipient,
std::shared_ptr<Keyring> keyring);
void
validate_register_service_node_parameters(
const std::string& service_node_key,
const service_nodes::registration_details& registration,
cryptonote::hf hf_version
);
PendingTransaction
create_register_service_node_transaction(
const uint64_t fee,
const std::vector<std::string>& addresses,
const std::vector<uint64_t>& amounts,
const uint64_t registration_hardfork,
const std::string& service_node_key,
const std::string& signature_str,
const cryptonote::tx_destination_entry& change_recipient,
std::shared_ptr<Keyring> keyring);
void
validate_stake_parameters(
const std::string& service_node_key,
uint64_t& amount,
const cryptonote::tx_destination_entry& change_recipient
);
PendingTransaction
create_stake_transaction(
const std::string& destination,
const std::string& service_node_key,
const uint64_t requested_amount,
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;

View File

@ -130,6 +130,12 @@ def eat_ons_data():
return ons_data
def eat_uint64_t():
return eat_uint(8)
def eat_uint8_t():
return eat_uint(1)
def eat_uint(size_in_bytes):
global tx_extra_list
amount = ''.join(tx_extra_list[:8*2])
tx_extra_list = tx_extra_list[8*2:]
@ -140,13 +146,119 @@ def eat_burn():
global tx_extra_list
return {"amount": eat_uint64_t()}
def eat_varint():
global tx_extra_list
length = 0
x = 0
b = 1
for i in range(0, len(tx_extra_list), 2):
length = length + 2
z = int(''.join(tx_extra_list[i:i+2]), 16)
x += b * (z & ~0x80)
if z & 0x80 == 0:
break
b <<= 7
tx_extra_list = tx_extra_list[length:]
return x
def eat_service_node_contributor():
global tx_extra_list
spend_public_key = ''.join(tx_extra_list[:64])
tx_extra_list = tx_extra_list[64:]
view_public_key = ''.join(tx_extra_list[:64])
tx_extra_list = tx_extra_list[64:]
return {"spend_public_key": spend_public_key,
"view_public_key": view_public_key }
def eat_service_node_pubkey():
global tx_extra_list
public_key = ''.join(tx_extra_list[:64])
tx_extra_list = tx_extra_list[64:]
return {"service_node_pubkey": public_key}
def eat_list(elem_size):
global tx_extra_list
results = []
size_hex = ''.join(tx_extra_list[:2])
tx_extra_list = tx_extra_list[2:]
size = int(size_hex, 16)
for i in range(size):
result = ''.join(tx_extra_list[:elem_size])
tx_extra_list = tx_extra_list[elem_size:]
results.append(result)
return results
def eat_uint64_t_list():
global tx_extra_list
results = []
size_hex = ''.join(tx_extra_list[:2])
tx_extra_list = tx_extra_list[2:]
size = int(size_hex, 16)
for i in range(size):
result = eat_uint64_t()
results.append(result)
return results
def eat_varint_list():
global tx_extra_list
results = []
size_hex = ''.join(tx_extra_list[:2])
tx_extra_list = tx_extra_list[2:]
size = int(size_hex, 16)
for i in range(size):
result = eat_varint()
results.append(result)
return results
def eat_service_node_registration():
global tx_extra_list
public_spend_keys = eat_list(64)
public_view_keys = eat_list(64)
fee = eat_uint64_t()
amounts= eat_varint_list()
hf = eat_uint8_t()
signature = ''.join(tx_extra_list[:128])
tx_extra_list = tx_extra_list[128:]
return {"public_spend_keys": public_spend_keys,
"public_view_keys": public_view_keys,
"fee": fee,
"amounts":amounts,
"hf":hf,
"signature": signature}
def eat_tx_secret_key():
global tx_extra_list
tx_secret_key = ''.join(tx_extra_list[:64])
tx_extra_list = tx_extra_list[64:]
return {"tx_secret_key": tx_secret_key}
def eat_tx_key_image_proofs():
global tx_extra_list
size_hex = ''.join(tx_extra_list[:2])
tx_extra_list = tx_extra_list[2:]
size = int(size_hex, 16)
key_images = []
signatures = []
for i in range(size):
tx_key_image = ''.join(tx_extra_list[:64])
key_images.append(tx_key_image)
tx_extra_list = tx_extra_list[64:]
signature = ''.join(tx_extra_list[:128])
signatures.append(signature)
tx_extra_list = tx_extra_list[128:]
return {"key_images": key_images,
"signatures": signatures}
eat_data_functions = {
"TX_EXTRA_TAG_PUBKEY": eat_pubkey_data,
"TX_EXTRA_NONCE": eat_nonce_data,
"TX_EXTRA_TAG_OXEN_NAME_SYSTEM": eat_ons_data,
"TX_EXTRA_TAG_BURN": eat_burn
"TX_EXTRA_TAG_BURN": eat_burn,
"TX_EXTRA_TAG_SERVICE_NODE_CONTRIBUTOR": eat_service_node_contributor,
"TX_EXTRA_TAG_SERVICE_NODE_PUBKEY": eat_service_node_pubkey,
"TX_EXTRA_TAG_SERVICE_NODE_REGISTER": eat_service_node_registration,
"TX_EXTRA_TAG_TX_SECRET_KEY": eat_tx_secret_key,
"TX_EXTRA_TAG_TX_KEY_IMAGE_PROOFS": eat_tx_key_image_proofs
}
# Main loop that reads over every item