1
0
Fork 0
forked from lthn/blockchain

wallet: sweeping of bare unspent outputs implemented

This commit is contained in:
sowle 2024-04-02 22:00:53 +02:00
parent 097420a66f
commit 6e036356df
No known key found for this signature in database
GPG key ID: C07A24B2D89D49FC
8 changed files with 343 additions and 9 deletions

View file

@ -1,4 +1,4 @@
// Copyright (c) 2014-2018 Zano Project
// Copyright (c) 2014-2024 Zano Project
// Copyright (c) 2014-2018 The Louisdor Project
// Copyright (c) 2012-2013 The Cryptonote developers
// Distributed under the MIT/X11 software license, see the accompanying
@ -63,6 +63,11 @@ namespace tools
}
bool password_container::read_password(const std::string& prompt_text)
{
return read_input(prompt_text, '*');
}
bool password_container::read_input(const std::string& prompt_text, char char_to_replace_user_input /* = '\0' */)
{
clear();
@ -70,7 +75,7 @@ namespace tools
if (is_cin_tty())
{
std::cout << prompt_text;
r = read_from_tty();
r = read_from_tty(char_to_replace_user_input);
}
else
{
@ -122,7 +127,7 @@ namespace tools
}
}
bool password_container::read_from_tty()
bool password_container::read_from_tty(char char_to_replace_user_input)
{
const char BACKSPACE = 8;
@ -162,7 +167,7 @@ namespace tools
else
{
m_password.push_back(ch);
std::cout << '*';
std::cout << (char_to_replace_user_input != '\0' ? char_to_replace_user_input : ch);
}
}
@ -198,7 +203,7 @@ namespace tools
}
}
bool password_container::read_from_tty()
bool password_container::read_from_tty(char char_to_replace_user_input)
{
const char BACKSPACE = 127;
@ -227,7 +232,7 @@ namespace tools
else
{
m_password.push_back(ch);
std::cout << '*';
std::cout << (char_to_replace_user_input != '\0' ? char_to_replace_user_input : ch);
}
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2014-2018 Zano Project
// Copyright (c) 2014-2024 Zano Project
// Copyright (c) 2014-2018 The Louisdor Project
// Copyright (c) 2012-2013 The Cryptonote developers
// Distributed under the MIT/X11 software license, see the accompanying
@ -24,13 +24,15 @@ namespace tools
void clear();
bool empty() const { return m_empty; }
const std::string& password() const { return m_password; }
const std::string& get_input() const { return m_password; } // TODO: refactor this
void password(std::string&& val) { m_password = std::move(val); m_empty = false; }
bool read_password();
bool read_password(const std::string& prompt_text);
bool read_input(const std::string& prompt_text, char char_to_replace_user_input = '\0');
private:
bool read_from_file();
bool read_from_tty();
bool read_from_tty(char char_to_replace_user_input);
private:
bool m_empty;

View file

@ -315,6 +315,7 @@ simple_wallet::simple_wallet()
m_cmd_binder.set_handler("scan_transfers_for_ki", boost::bind(&simple_wallet::scan_transfers_for_ki, this,ph::_1), "Rescan transfers for key image");
m_cmd_binder.set_handler("print_utxo_distribution", boost::bind(&simple_wallet::print_utxo_distribution, this,ph::_1), "Prints utxo distribution");
m_cmd_binder.set_handler("sweep_below", boost::bind(&simple_wallet::sweep_below, this,ph::_1), "sweep_below <mixin_count> <address> <amount_lower_limit> [payment_id] - Tries to transfers all coins with amount below the given limit to the given address");
m_cmd_binder.set_handler("sweep_bare_outs", boost::bind(&simple_wallet::sweep_bare_outs, this,ph::_1), "sweep_bare_outs - Transfers all bare unspent outputs to itself. Uses several txs if necessary.");
m_cmd_binder.set_handler("address", boost::bind(&simple_wallet::print_address, this,ph::_1), "Show current wallet public address");
m_cmd_binder.set_handler("integrated_address", boost::bind(&simple_wallet::integrated_address, this,ph::_1), "integrated_address [<payment_id>|<integrated_address] - encodes given payment_id along with wallet's address into an integrated address (random payment_id will be used if none is provided). Decodes given integrated_address into standard address");
@ -2511,8 +2512,129 @@ bool simple_wallet::sweep_below(const std::vector<std::string> &args)
SIMPLE_WALLET_CATCH_TRY_ENTRY();
return true;
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::sweep_bare_outs(const std::vector<std::string> &args)
{
CONFIRM_WITH_PASSWORD();
SIMPLE_WALLET_BEGIN_TRY_ENTRY();
bool r = false;
if (args.size() > 1)
{
fail_msg_writer() << "Invalid number of agruments given";
return true;
}
currency::account_public_address target_address = m_wallet->get_account().get_public_address();
currency::payment_id_t integrated_payment_id{};
if (args.size() == 1)
{
if (!m_wallet->get_transfer_address(args[0], target_address, integrated_payment_id))
{
fail_msg_writer() << "Unable to parse address from " << args[1];
return true;
}
if (!integrated_payment_id.empty())
{
fail_msg_writer() << "Payment id is not supported. Please, don't use integrated address with this command.";
return true;
}
}
if (!m_wallet->has_bare_unspent_outputs())
{
success_msg_writer(true) << "This wallet doesn't have bare unspent outputs.\nNothing to do. Everything looks good.";
return true;
}
std::vector<tools::wallet2::batch_of_bare_unspent_outs> groups;
m_wallet->get_bare_unspent_outputs_stats(groups);
if (groups.empty())
{
uint64_t unlocked_balance = 0;
uint64_t balance = m_wallet->balance(unlocked_balance);
if (balance < COIN)
success_msg_writer(false) << "Looks like it's not enough coins to perform this operation. Transferring " << print_money_brief(TX_MINIMUM_FEE) << " ZANO or more to this wallet may help.";
else if (unlocked_balance < COIN)
success_msg_writer(false) << "Not enough spendable outputs to perform this operation. Please, try again later.";
else
{
success_msg_writer(false) << "This operation couldn't be performed for some reason. Please, copy simplewallet's log file and ask for support. Nothing was done.";
LOG_PRINT_L0("strange situation: balance: " << print_money_brief(balance) << ", unlocked_balance: " << print_money_brief(unlocked_balance) << " but get_bare_unspent_outputs_stats returned empty result");
}
return true;
}
size_t i = 0, total_bare_outs = 0;
uint64_t total_mount = 0;
std::stringstream details_ss;
for(auto &g : groups)
{
details_ss << std::setw(2) << i << ": ";
for (auto& tid: g.tids)
{
tools::transfer_details td{};
CHECK_AND_ASSERT_THROW_MES(m_wallet->get_transfer_info_by_index(tid, td), "get_transfer_info_by_index failed with index " << tid);
details_ss << tid << " (" << print_money_brief(td.m_amount) << "), ";
total_mount += td.m_amount;
}
if (g.additional_tid)
details_ss << "additional tid: " << g.tids.back() << "( " << print_money_brief(g.additional_tid_amount) << ")";
else
details_ss.seekp(-2, std::ios_base::end);
details_ss << ENDL;
++i;
total_bare_outs += g.tids.size();
}
LOG_PRINT_L1("bare UTXO:" << ENDL << details_ss.str());
success_msg_writer(true) << "This wallet contains " << total_bare_outs << " bare outputs with total amount of " << print_money_brief(total_mount) <<
". They can be converted in " << groups.size() << " transaction" << (groups.size() > 1 ? "s" : "") << ", with total fee = " << print_money_brief(TX_DEFAULT_FEE * i) << ".";
if (target_address != m_wallet->get_account().get_public_address())
message_writer(epee::log_space::console_color_yellow, false) << print_money_brief(total_mount) << " coins will be sent to address " << get_account_address_as_str(target_address);
tools::password_container reader;
if (!reader.read_input("Would you like to continue? (y/yes/n/no):\n") || (reader.get_input() != "y" && reader.get_input() != "yes"))
{
success_msg_writer(false) << "Operatation terminated as requested by user.";
return true;
}
size_t total_tx_sent = 0;
uint64_t total_fee_spent = 0;
uint64_t total_amount_sent = 0;
auto on_tx_sent_callback = [&](size_t batch_index, const currency::transaction& tx, uint64_t amount, uint64_t fee, bool sent_ok, const std::string& err) {
auto mw = success_msg_writer(false);
mw << std::setw(2) << batch_index << ": transaction ";
if (!sent_ok)
{
mw << "failed (" << err << ")";
return;
}
mw << get_transaction_hash(tx) << ", fee: " << print_money_brief(fee) << ", amount: " << print_money_brief(amount);
++total_tx_sent;
total_fee_spent += fee;
total_amount_sent += amount;
};
if (!m_wallet->sweep_bare_unspent_outputs(target_address, groups, on_tx_sent_callback))
{
auto mw = fail_msg_writer();
mw << "Operatation failed.";
if (total_tx_sent > 0)
mw << " However, " << total_tx_sent << " transaction" << (total_tx_sent == 1 ? " was" : "s were") << " successfully sent, " << print_money_brief(total_amount_sent) << " coins transferred, and " << print_money_brief(total_fee_spent) << " was spent for fees.";
}
else
{
success_msg_writer(true) << "Operatation succeeded. " << ENDL << total_tx_sent << " transaction" << (total_tx_sent == 1 ? " was" : "s were") << " successfully sent, " << print_money_brief(total_amount_sent) << " coins transferred, and " << print_money_brief(total_fee_spent) << " was spent for fees.";
}
SIMPLE_WALLET_CATCH_TRY_ENTRY();
return true;
}
//----------------------------------------------------------------------------------------------------
uint64_t
get_tick_count__()

View file

@ -1,4 +1,4 @@
// Copyright (c) 2014-2018 Zano Project
// Copyright (c) 2014-2024 Zano Project
// Copyright (c) 2014-2018 The Louisdor Project
// Copyright (c) 2012-2013 The Cryptonote developers
// Distributed under the MIT/X11 software license, see the accompanying
@ -87,6 +87,7 @@ namespace currency
bool sign_transfer(const std::vector<std::string> &args);
bool submit_transfer(const std::vector<std::string> &args);
bool sweep_below(const std::vector<std::string> &args);
bool sweep_bare_outs(const std::vector<std::string> &args);
bool tor_enable(const std::vector<std::string> &args);
bool tor_disable(const std::vector<std::string> &args);
bool deploy_new_asset(const std::vector<std::string> &args);

View file

@ -2200,6 +2200,189 @@ bool wallet2::has_related_alias_entry_unconfirmed(const currency::transaction& t
return false;
}
//----------------------------------------------------------------------------------------------------
#define HARDFORK_04_TIMESTAMP_ACTUAL 1711021795ull // block 2555000, 2024-03-21 11:49:55 UTC
bool wallet2::has_bare_unspent_outputs() const
{
if (m_account.get_createtime() > HARDFORK_04_TIMESTAMP_ACTUAL)
return false;
[[maybe_unused]] uint64_t bal = 0;
if (!m_has_bare_unspent_outputs.has_value())
bal = balance();
WLT_THROW_IF_FALSE_WALLET_INT_ERR_EX(m_has_bare_unspent_outputs.has_value(), "m_has_bare_unspent_outputs has no value after balance()");
return m_has_bare_unspent_outputs.value();
}
//----------------------------------------------------------------------------------------------------
#define MAX_INPUTS_FOR_SIMPLE_TX_EURISTIC 20
bool wallet2::get_bare_unspent_outputs_stats(std::vector<batch_of_bare_unspent_outs>& tids_grouped_by_txs) const
{
tids_grouped_by_txs.clear();
// 1/3. Populate a list of bare unspent outputs
std::unordered_map<crypto::hash, std::vector<size_t>> buo_ids; // tx hash -> Bare Unspent Outs list
for(size_t tid = 0; tid != m_transfers.size(); ++tid)
{
const auto& td = m_transfers[tid];
if (!td.is_zc() && td.is_spendable())
{
buo_ids[td.tx_hash()].push_back(tid);
}
}
if (buo_ids.empty())
return true;
// 2/3. Split them into groups
tids_grouped_by_txs.emplace_back();
for(auto& buo_el : buo_ids)
{
if (tids_grouped_by_txs.back().tids.size() + buo_el.second.size() > MAX_INPUTS_FOR_SIMPLE_TX_EURISTIC)
tids_grouped_by_txs.emplace_back();
for(auto& tid : buo_el.second)
{
if (tids_grouped_by_txs.back().tids.size() >= MAX_INPUTS_FOR_SIMPLE_TX_EURISTIC)
tids_grouped_by_txs.emplace_back();
tids_grouped_by_txs.back().tids.push_back(tid);
tids_grouped_by_txs.back().total_amount += m_transfers[tid].m_amount;
}
}
// 3/3. Iterate through groups and check whether total amount is big enough to cover min fee.
// Add additional zc output if not.
std::multimap<uint64_t, size_t> usable_zc_outs_tids; // grouped by amount
bool usable_zc_outs_tids_precalculated = false;
auto precalculate_usable_zc_outs_if_needed = [&](){
if (usable_zc_outs_tids_precalculated)
return;
size_t decoys = is_auditable() ? 0 : m_core_runtime_config.hf4_minimum_mixins;
for(size_t tid = 0; tid != m_transfers.size(); ++tid)
{
auto& td = m_transfers[tid];
if (td.is_zc() && td.is_native_coin() && is_transfer_ready_to_go(td, decoys))
usable_zc_outs_tids.insert(std::make_pair(td.m_amount, tid));
}
usable_zc_outs_tids_precalculated = true;
};
std::unordered_set<size_t> used_zc_outs;
for(auto it = tids_grouped_by_txs.begin(); it != tids_grouped_by_txs.end(); )
{
auto& group = *it;
if (group.total_amount < TX_MINIMUM_FEE)
{
precalculate_usable_zc_outs_if_needed();
uint64_t min_required_amount = TX_MINIMUM_FEE - group.total_amount;
auto jt = usable_zc_outs_tids.lower_bound(min_required_amount);
bool found = false;
while(jt != usable_zc_outs_tids.end())
{
WLT_THROW_IF_FALSE_WALLET_INT_ERR_EX(jt->first >= min_required_amount, "jt->first=" << jt->first << ", min_required_amount=" << min_required_amount);
if (used_zc_outs.count(jt->second) == 0)
{
group.tids.push_back(jt->second);
used_zc_outs.insert(jt->second);
group.additional_tid = true;
group.additional_tid_amount = jt->first;
found = true;
break;
}
++jt;
}
if (!found)
{
// no usable outs for required amount, remove this group and go to the next
it = tids_grouped_by_txs.erase(it);
continue;
}
}
++it;
}
return true;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::sweep_bare_unspent_outputs(const currency::account_public_address& target_address, const std::vector<batch_of_bare_unspent_outs>& tids_grouped_by_txs,
std::function<void(size_t batch_index, const currency::transaction& tx, uint64_t amount, uint64_t fee, bool sent_ok, const std::string& err)> on_tx_sent)
{
if (m_watch_only)
return false;
size_t decoys_count = is_auditable() ? 0 : CURRENCY_DEFAULT_DECOY_SET_SIZE;
bool send_to_network = true;
size_t batch_index = 0;
for(const batch_of_bare_unspent_outs& group : tids_grouped_by_txs)
{
currency::finalized_tx ftx{};
currency::finalize_tx_param ftp{};
ftp.pevents_dispatcher = &m_debug_events_dispatcher;
ftp.tx_version = this->get_current_tx_version();
if (!prepare_tx_sources(decoys_count, ftp.sources, group.tids))
{
on_tx_sent(batch_index, transaction{}, 0, 0, false, "sources for tx couldn't be prepared");
LOG_PRINT_L0("prepare_tx_sources failed, batch_index = " << batch_index);
return false;
}
uint64_t fee = TX_DEFAULT_FEE;
std::vector<tx_destination_entry> destinations{tx_destination_entry(group.total_amount + group.additional_tid_amount - fee, target_address)};
assets_selection_context needed_money_map{std::make_pair(native_coin_asset_id, selection_for_amount{group.total_amount + group.additional_tid_amount, group.total_amount + group.additional_tid_amount})};
try
{
prepare_tx_destinations(needed_money_map, get_current_split_strategy(), tx_dust_policy{}, destinations, 0 /* tx_flags */, ftp.prepared_destinations);
}
catch(...)
{
on_tx_sent(batch_index, transaction{}, 0, 0, false, "destinations for tx couldn't be prepared");
LOG_PRINT_L0("prepare_tx_destinations failed, batch_index = " << batch_index);
return false;
}
mark_transfers_as_spent(ftp.selected_transfers, std::string("sweep bare UTXO, tx: ") + epee::string_tools::pod_to_hex(get_transaction_hash(ftx.tx)));
try
{
finalize_transaction(ftp, ftx, send_to_network);
on_tx_sent(batch_index, ftx.tx, group.total_amount + group.additional_tid_amount, fee, true, std::string());
}
catch(std::exception& e)
{
clear_transfers_from_flag(ftp.selected_transfers, WALLET_TRANSFER_DETAIL_FLAG_SPENT, std::string("exception on sweep bare UTXO, tx: ") + epee::string_tools::pod_to_hex(get_transaction_hash(ftx.tx)));
on_tx_sent(batch_index, transaction{}, 0, 0, false, e.what());
return false;
}
++batch_index;
}
return true;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::sweep_bare_unspent_outputs(const currency::account_public_address& target_address, const std::vector<batch_of_bare_unspent_outs>& tids_grouped_by_txs,
size_t& total_txs_sent, uint64_t& total_amount_sent, uint64_t& total_fee_spent)
{
total_txs_sent = 0;
total_amount_sent = 0;
total_fee_spent = 0;
auto on_tx_sent_callback = [&](size_t batch_index, const currency::transaction& tx, uint64_t amount, uint64_t fee, bool sent_ok, const std::string& err) {
if (sent_ok)
{
++total_txs_sent;
total_fee_spent += fee;
total_amount_sent += amount;
}
};
return sweep_bare_unspent_outputs(target_address, tids_grouped_by_txs, on_tx_sent_callback);
}
//----------------------------------------------------------------------------------------------------
uint64_t wallet2::get_directly_spent_transfer_index_by_input_in_tracking_wallet(const currency::txin_to_key& intk)
{
return get_directly_spent_transfer_index_by_input_in_tracking_wallet(intk.amount, intk.key_offsets);
@ -3444,6 +3627,7 @@ uint64_t wallet2::balance(const crypto::public_key& asset_id) const
bool wallet2::balance(std::unordered_map<crypto::public_key, wallet_public::asset_balance_entry_base>& balances, uint64_t& mined) const
{
mined = 0;
m_has_bare_unspent_outputs = false;
for(auto& td : m_transfers)
{
@ -3464,6 +3648,9 @@ bool wallet2::balance(std::unordered_map<crypto::public_key, wallet_public::asse
mined += CURRENCY_BLOCK_REWARD; //this code would work only for cases where block reward is full. For reduced block rewards might need more flexible code (TODO)
}
}
if (!td.is_zc())
m_has_bare_unspent_outputs = true;
}
}

View file

@ -158,6 +158,7 @@ namespace tools
std::atomic<uint64_t> m_last_sync_percent = 0;
mutable uint64_t m_current_wallet_file_size = 0;
bool m_use_assets_whitelisting = true;
mutable std::optional<bool> m_has_bare_unspent_outputs; // recalculated each time the balance() is called
//===============================================================
@ -342,6 +343,13 @@ namespace tools
mutable crypto::hash tx_hash_ = currency::null_hash;
};
struct batch_of_bare_unspent_outs
{
std::vector<size_t> tids;
uint64_t total_amount = 0;
bool additional_tid = false; // additional zc transfer if total_amount < min fee
uint64_t additional_tid_amount = 0;
};
@ -377,6 +385,12 @@ namespace tools
void set_do_rise_transfer(bool do_rise) { m_do_rise_transfer = do_rise; }
bool has_related_alias_entry_unconfirmed(const currency::transaction& tx);
bool has_bare_unspent_outputs() const;
bool get_bare_unspent_outputs_stats(std::vector<batch_of_bare_unspent_outs>& buo_txs) const;
bool sweep_bare_unspent_outputs(const currency::account_public_address& target_address, const std::vector<batch_of_bare_unspent_outs>& tids_grouped_by_txs,
std::function<void(size_t batch_index, const currency::transaction& tx, uint64_t amount, uint64_t fee, bool sent_ok, const std::string& err)> on_tx_sent);
bool sweep_bare_unspent_outputs(const currency::account_public_address& target_address, const std::vector<batch_of_bare_unspent_outs>& tids_grouped_by_txs,
size_t& total_txs_sent, uint64_t& total_amount_sent, uint64_t& total_fee);
void handle_unconfirmed_tx(process_transaction_context& ptc);
void scan_tx_pool(bool& has_related_alias_in_unconfirmed);
void refresh();

View file

@ -20,6 +20,7 @@ namespace tools
wi.path = epee::string_encoding::wstring_to_utf8(w.get_wallet_path());
wi.is_auditable = w.is_auditable();
wi.is_watch_only = w.is_watch_only();
wi.has_bare_unspent_outputs = w.has_bare_unspent_outputs();
return true;
}

View file

@ -1550,6 +1550,7 @@ namespace wallet_public
std::string path;
bool is_auditable;
bool is_watch_only;
bool has_bare_unspent_outputs;
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(balances)
@ -1559,6 +1560,7 @@ namespace wallet_public
KV_SERIALIZE(path)
KV_SERIALIZE(is_auditable)
KV_SERIALIZE(is_watch_only)
KV_SERIALIZE(has_bare_unspent_outputs)
END_KV_SERIALIZE_MAP()
};