wallet: new transaction construction algorithm
It should avoid a lot of the issues sending more than half the wallet's contents due to change. Actual output selection is still random. Changing this would improve the matching of transaction amounts to output sizes, but may have non obvious effects on blockchain analysis. Mapped to the new transfer_new command in simplewallet, and transfer uses the existing algorithm. To use in RPC, add "new_algorithm: true" in the transfer_split JSON command. It is not used in the transfer command.
This commit is contained in:
parent
1737fec297
commit
988fe1f843
6 changed files with 438 additions and 5 deletions
|
@ -353,6 +353,7 @@ simple_wallet::simple_wallet()
|
||||||
m_cmd_binder.set_handler("payments", boost::bind(&simple_wallet::show_payments, this, _1), tr("payments <payment_id_1> [<payment_id_2> ... <payment_id_N>] - Show payments <payment_id_1>, ... <payment_id_N>"));
|
m_cmd_binder.set_handler("payments", boost::bind(&simple_wallet::show_payments, this, _1), tr("payments <payment_id_1> [<payment_id_2> ... <payment_id_N>] - Show payments <payment_id_1>, ... <payment_id_N>"));
|
||||||
m_cmd_binder.set_handler("bc_height", boost::bind(&simple_wallet::show_blockchain_height, this, _1), tr("Show blockchain height"));
|
m_cmd_binder.set_handler("bc_height", boost::bind(&simple_wallet::show_blockchain_height, this, _1), tr("Show blockchain height"));
|
||||||
m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::transfer, this, _1), tr("transfer [<mixin_count>] <addr_1> <amount_1> [<addr_2> <amount_2> ... <addr_N> <amount_N>] [payment_id] - Transfer <amount_1>,... <amount_N> to <address_1>,... <address_N>, respectively. <mixin_count> is the number of transactions yours is indistinguishable from (from 0 to maximum available)"));
|
m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::transfer, this, _1), tr("transfer [<mixin_count>] <addr_1> <amount_1> [<addr_2> <amount_2> ... <addr_N> <amount_N>] [payment_id] - Transfer <amount_1>,... <amount_N> to <address_1>,... <address_N>, respectively. <mixin_count> is the number of transactions yours is indistinguishable from (from 0 to maximum available)"));
|
||||||
|
m_cmd_binder.set_handler("transfer_new", boost::bind(&simple_wallet::transfer_new, this, _1), tr("Same as transfer, but using a new transaction building algorithm"));
|
||||||
m_cmd_binder.set_handler("sweep_dust", boost::bind(&simple_wallet::sweep_dust, this, _1), tr("Send all dust outputs to the same address with mixin 0"));
|
m_cmd_binder.set_handler("sweep_dust", boost::bind(&simple_wallet::sweep_dust, this, _1), tr("Send all dust outputs to the same address with mixin 0"));
|
||||||
m_cmd_binder.set_handler("set_log", boost::bind(&simple_wallet::set_log, this, _1), tr("set_log <level> - Change current log detalization level, <level> is a number 0-4"));
|
m_cmd_binder.set_handler("set_log", boost::bind(&simple_wallet::set_log, this, _1), tr("set_log <level> - Change current log detalization level, <level> is a number 0-4"));
|
||||||
m_cmd_binder.set_handler("address", boost::bind(&simple_wallet::print_address, this, _1), tr("Show current wallet public address"));
|
m_cmd_binder.set_handler("address", boost::bind(&simple_wallet::print_address, this, _1), tr("Show current wallet public address"));
|
||||||
|
@ -1254,7 +1255,7 @@ bool simple_wallet::show_blockchain_height(const std::vector<std::string>& args)
|
||||||
}
|
}
|
||||||
|
|
||||||
//----------------------------------------------------------------------------------------------------
|
//----------------------------------------------------------------------------------------------------
|
||||||
bool simple_wallet::transfer(const std::vector<std::string> &args_)
|
bool simple_wallet::transfer_main(bool new_algorithm, const std::vector<std::string> &args_)
|
||||||
{
|
{
|
||||||
if (!try_connect_to_daemon())
|
if (!try_connect_to_daemon())
|
||||||
return true;
|
return true;
|
||||||
|
@ -1410,7 +1411,11 @@ bool simple_wallet::transfer(const std::vector<std::string> &args_)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// figure out what tx will be necessary
|
// figure out what tx will be necessary
|
||||||
auto ptx_vector = m_wallet->create_transactions(dsts, fake_outs_count, 0 /* unlock_time */, 0 /* unused fee arg*/, extra);
|
std::vector<tools::wallet2::pending_tx> ptx_vector;
|
||||||
|
if (new_algorithm)
|
||||||
|
ptx_vector = m_wallet->create_transactions_2(dsts, fake_outs_count, 0 /* unlock_time */, 0 /* unused fee arg*/, extra);
|
||||||
|
else
|
||||||
|
ptx_vector = m_wallet->create_transactions(dsts, fake_outs_count, 0 /* unlock_time */, 0 /* unused fee arg*/, extra);
|
||||||
|
|
||||||
// if more than one tx necessary, prompt user to confirm
|
// if more than one tx necessary, prompt user to confirm
|
||||||
if (m_wallet->always_confirm_transfers() || ptx_vector.size() > 1)
|
if (m_wallet->always_confirm_transfers() || ptx_vector.size() > 1)
|
||||||
|
@ -1523,6 +1528,16 @@ bool simple_wallet::transfer(const std::vector<std::string> &args_)
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
//----------------------------------------------------------------------------------------------------
|
||||||
|
bool simple_wallet::transfer(const std::vector<std::string> &args_)
|
||||||
|
{
|
||||||
|
return transfer_main(false, args_);
|
||||||
|
}
|
||||||
|
//----------------------------------------------------------------------------------------------------
|
||||||
|
bool simple_wallet::transfer_new(const std::vector<std::string> &args_)
|
||||||
|
{
|
||||||
|
return transfer_main(true, args_);
|
||||||
|
}
|
||||||
|
|
||||||
//----------------------------------------------------------------------------------------------------
|
//----------------------------------------------------------------------------------------------------
|
||||||
bool simple_wallet::sweep_dust(const std::vector<std::string> &args_)
|
bool simple_wallet::sweep_dust(const std::vector<std::string> &args_)
|
||||||
|
|
|
@ -108,7 +108,9 @@ namespace cryptonote
|
||||||
bool show_incoming_transfers(const std::vector<std::string> &args);
|
bool show_incoming_transfers(const std::vector<std::string> &args);
|
||||||
bool show_payments(const std::vector<std::string> &args);
|
bool show_payments(const std::vector<std::string> &args);
|
||||||
bool show_blockchain_height(const std::vector<std::string> &args);
|
bool show_blockchain_height(const std::vector<std::string> &args);
|
||||||
|
bool transfer_main(bool new_algorithm, const std::vector<std::string> &args);
|
||||||
bool transfer(const std::vector<std::string> &args);
|
bool transfer(const std::vector<std::string> &args);
|
||||||
|
bool transfer_new(const std::vector<std::string> &args);
|
||||||
bool sweep_dust(const std::vector<std::string> &args);
|
bool sweep_dust(const std::vector<std::string> &args);
|
||||||
std::vector<std::vector<cryptonote::tx_destination_entry>> split_amounts(
|
std::vector<std::vector<cryptonote::tx_destination_entry>> split_amounts(
|
||||||
std::vector<cryptonote::tx_destination_entry> dsts, size_t num_splits
|
std::vector<cryptonote::tx_destination_entry> dsts, size_t num_splits
|
||||||
|
|
|
@ -59,6 +59,12 @@ extern "C"
|
||||||
}
|
}
|
||||||
using namespace cryptonote;
|
using namespace cryptonote;
|
||||||
|
|
||||||
|
// used to choose when to stop adding outputs to a tx
|
||||||
|
#define APPROXIMATE_INPUT_BYTES 80
|
||||||
|
|
||||||
|
// used to target a given block size (additional outputs may be added on top to build fee)
|
||||||
|
#define TX_SIZE_TARGET(bytes) (bytes*2/3)
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
void do_prepare_file_names(const std::string& file_path, std::string& keys_file, std::string& wallet_file)
|
void do_prepare_file_names(const std::string& file_path, std::string& keys_file, std::string& wallet_file)
|
||||||
|
@ -936,11 +942,10 @@ bool wallet2::is_tx_spendtime_unlocked(uint64_t unlock_time) const
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
template<typename T>
|
template<typename T>
|
||||||
T pop_random_value(std::vector<T>& vec)
|
T pop_index(std::vector<T>& vec, size_t idx)
|
||||||
{
|
{
|
||||||
CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty");
|
CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty");
|
||||||
|
|
||||||
size_t idx = crypto::rand<size_t>() % vec.size();
|
|
||||||
T res = vec[idx];
|
T res = vec[idx];
|
||||||
if (idx + 1 != vec.size())
|
if (idx + 1 != vec.size())
|
||||||
{
|
{
|
||||||
|
@ -950,6 +955,15 @@ namespace
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T pop_random_value(std::vector<T>& vec)
|
||||||
|
{
|
||||||
|
CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty");
|
||||||
|
|
||||||
|
size_t idx = crypto::rand<size_t>() % vec.size();
|
||||||
|
return pop_index (vec, idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//----------------------------------------------------------------------------------------------------
|
//----------------------------------------------------------------------------------------------------
|
||||||
// Select random input sources for transaction.
|
// Select random input sources for transaction.
|
||||||
|
@ -1277,6 +1291,397 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count,
|
||||||
|
uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx)
|
||||||
|
{
|
||||||
|
using namespace cryptonote;
|
||||||
|
// throw if attempting a transaction with no destinations
|
||||||
|
THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination);
|
||||||
|
|
||||||
|
uint64_t needed_money = fee;
|
||||||
|
LOG_PRINT_L2("transfer: starting with fee " << print_money (needed_money));
|
||||||
|
|
||||||
|
// calculate total amount being sent to all destinations
|
||||||
|
// throw if total amount overflows uint64_t
|
||||||
|
BOOST_FOREACH(auto& dt, dsts)
|
||||||
|
{
|
||||||
|
THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination);
|
||||||
|
needed_money += dt.amount;
|
||||||
|
LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money));
|
||||||
|
THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, fee, m_testnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t found_money = 0;
|
||||||
|
BOOST_FOREACH(auto it, selected_transfers)
|
||||||
|
{
|
||||||
|
found_money += it->amount();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_PRINT_L2("wanted " << print_money(needed_money) << ", found " << print_money(found_money) << ", fee " << print_money(fee));
|
||||||
|
THROW_WALLET_EXCEPTION_IF(found_money < needed_money, error::not_enough_money, found_money, needed_money - fee, fee);
|
||||||
|
|
||||||
|
typedef COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::out_entry out_entry;
|
||||||
|
typedef cryptonote::tx_source_entry::output_entry tx_output_entry;
|
||||||
|
|
||||||
|
COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::response daemon_resp = AUTO_VAL_INIT(daemon_resp);
|
||||||
|
if(fake_outputs_count)
|
||||||
|
{
|
||||||
|
COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::request req = AUTO_VAL_INIT(req);
|
||||||
|
req.outs_count = fake_outputs_count + 1;// add one to make possible (if need) to skip real output key
|
||||||
|
BOOST_FOREACH(transfer_container::iterator it, selected_transfers)
|
||||||
|
{
|
||||||
|
THROW_WALLET_EXCEPTION_IF(it->m_tx.vout.size() <= it->m_internal_output_index, error::wallet_internal_error,
|
||||||
|
"m_internal_output_index = " + std::to_string(it->m_internal_output_index) +
|
||||||
|
" is greater or equal to outputs count = " + std::to_string(it->m_tx.vout.size()));
|
||||||
|
req.amounts.push_back(it->amount());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/getrandom_outs.bin", req, daemon_resp, m_http_client, 200000);
|
||||||
|
THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getrandom_outs.bin");
|
||||||
|
THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getrandom_outs.bin");
|
||||||
|
THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::get_random_outs_error, daemon_resp.status);
|
||||||
|
THROW_WALLET_EXCEPTION_IF(daemon_resp.outs.size() != selected_transfers.size(), error::wallet_internal_error,
|
||||||
|
"daemon returned wrong response for getrandom_outs.bin, wrong amounts count = " +
|
||||||
|
std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(selected_transfers.size()));
|
||||||
|
|
||||||
|
std::vector<COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount> scanty_outs;
|
||||||
|
BOOST_FOREACH(COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& amount_outs, daemon_resp.outs)
|
||||||
|
{
|
||||||
|
if (amount_outs.outs.size() < fake_outputs_count)
|
||||||
|
{
|
||||||
|
scanty_outs.push_back(amount_outs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
THROW_WALLET_EXCEPTION_IF(!scanty_outs.empty(), error::not_enough_outs_to_mix, scanty_outs, fake_outputs_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
//prepare inputs
|
||||||
|
size_t i = 0;
|
||||||
|
std::vector<cryptonote::tx_source_entry> sources;
|
||||||
|
BOOST_FOREACH(transfer_container::iterator it, selected_transfers)
|
||||||
|
{
|
||||||
|
sources.resize(sources.size()+1);
|
||||||
|
cryptonote::tx_source_entry& src = sources.back();
|
||||||
|
transfer_details& td = *it;
|
||||||
|
src.amount = td.amount();
|
||||||
|
//paste mixin transaction
|
||||||
|
if(daemon_resp.outs.size())
|
||||||
|
{
|
||||||
|
daemon_resp.outs[i].outs.sort([](const out_entry& a, const out_entry& b){return a.global_amount_index < b.global_amount_index;});
|
||||||
|
BOOST_FOREACH(out_entry& daemon_oe, daemon_resp.outs[i].outs)
|
||||||
|
{
|
||||||
|
if(td.m_global_output_index == daemon_oe.global_amount_index)
|
||||||
|
continue;
|
||||||
|
tx_output_entry oe;
|
||||||
|
oe.first = daemon_oe.global_amount_index;
|
||||||
|
oe.second = daemon_oe.out_key;
|
||||||
|
src.outputs.push_back(oe);
|
||||||
|
if(src.outputs.size() >= fake_outputs_count)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//paste real transaction to the random index
|
||||||
|
auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a)
|
||||||
|
{
|
||||||
|
return a.first >= td.m_global_output_index;
|
||||||
|
});
|
||||||
|
//size_t real_index = src.outputs.size() ? (rand() % src.outputs.size() ):0;
|
||||||
|
tx_output_entry real_oe;
|
||||||
|
real_oe.first = td.m_global_output_index;
|
||||||
|
real_oe.second = boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key;
|
||||||
|
auto interted_it = src.outputs.insert(it_to_insert, real_oe);
|
||||||
|
src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx);
|
||||||
|
src.real_output = interted_it - src.outputs.begin();
|
||||||
|
src.real_output_in_tx_index = td.m_internal_output_index;
|
||||||
|
detail::print_source_entry(src);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts);
|
||||||
|
if (needed_money < found_money)
|
||||||
|
{
|
||||||
|
change_dts.addr = m_account.get_keys().m_account_address;
|
||||||
|
change_dts.amount = found_money - needed_money;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t dust = 0;
|
||||||
|
std::vector<cryptonote::tx_destination_entry> splitted_dsts;
|
||||||
|
destination_split_strategy(dsts, change_dts, dust_policy.dust_threshold, splitted_dsts, dust);
|
||||||
|
THROW_WALLET_EXCEPTION_IF(dust_policy.dust_threshold < dust, error::wallet_internal_error, "invalid dust value: dust = " +
|
||||||
|
std::to_string(dust) + ", dust_threshold = " + std::to_string(dust_policy.dust_threshold));
|
||||||
|
if (0 != dust && !dust_policy.add_to_fee)
|
||||||
|
{
|
||||||
|
splitted_dsts.push_back(cryptonote::tx_destination_entry(dust, dust_policy.addr_for_dust));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool r = cryptonote::construct_tx(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time);
|
||||||
|
THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, splitted_dsts, unlock_time, m_testnet);
|
||||||
|
THROW_WALLET_EXCEPTION_IF(m_upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, m_upper_transaction_size_limit);
|
||||||
|
|
||||||
|
std::string key_images;
|
||||||
|
bool all_are_txin_to_key = std::all_of(tx.vin.begin(), tx.vin.end(), [&](const txin_v& s_e) -> bool
|
||||||
|
{
|
||||||
|
CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false);
|
||||||
|
key_images += boost::to_string(in.k_image) + " ";
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, tx);
|
||||||
|
|
||||||
|
ptx.key_images = key_images;
|
||||||
|
ptx.fee = fee;
|
||||||
|
ptx.dust = dust;
|
||||||
|
ptx.tx = tx;
|
||||||
|
ptx.change_dts = change_dts;
|
||||||
|
ptx.selected_transfers = selected_transfers;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another implementation of transaction creation that is hopefully better
|
||||||
|
// While there is anything left to pay, it goes through random outputs and tries
|
||||||
|
// to fill the next destination/amount. If it fully fills it, it will use the
|
||||||
|
// remainder to try to fill the next one as well.
|
||||||
|
// The tx size if roughly estimated as a linear function of only inputs, and a
|
||||||
|
// new tx will be created when that size goes above a given fraction of the
|
||||||
|
// max tx size. At that point, more outputs may be added if the fee cannot be
|
||||||
|
// satisfied.
|
||||||
|
// If the next output in the next tx would go to the same destination (ie, we
|
||||||
|
// cut off at a tx boundary in the middle of paying a given destination), the
|
||||||
|
// fee will be carved out of the current input if possible, to avoid having to
|
||||||
|
// add another output just for the fee and getting change.
|
||||||
|
// This system allows for sending (almost) the entire balance, since it does
|
||||||
|
// not generate spurious change in all txes, thus decreasing the instantaneous
|
||||||
|
// usable balance.
|
||||||
|
std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector<uint8_t> extra)
|
||||||
|
{
|
||||||
|
std::vector<size_t> unused_transfers_indices;
|
||||||
|
std::vector<size_t> unused_dust_indices;
|
||||||
|
uint64_t needed_money;
|
||||||
|
uint64_t accumulated_fee, accumulated_outputs, accumulated_change;
|
||||||
|
struct TX {
|
||||||
|
std::list<transfer_container::iterator> selected_transfers;
|
||||||
|
std::vector<cryptonote::tx_destination_entry> dsts;
|
||||||
|
cryptonote::transaction tx;
|
||||||
|
pending_tx ptx;
|
||||||
|
size_t bytes;
|
||||||
|
|
||||||
|
void add(const account_public_address &addr, uint64_t amount) {
|
||||||
|
std::vector<cryptonote::tx_destination_entry>::iterator i;
|
||||||
|
i = std::find_if(dsts.begin(), dsts.end(), [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &addr, sizeof(addr)); });
|
||||||
|
if (i == dsts.end())
|
||||||
|
dsts.push_back(tx_destination_entry(amount,addr));
|
||||||
|
else
|
||||||
|
i->amount += amount;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::vector<TX> txes;
|
||||||
|
bool adding_fee; // true if new outputs go towards fee, rather than destinations
|
||||||
|
uint64_t needed_fee, available_for_fee = 0;
|
||||||
|
|
||||||
|
// throw if attempting a transaction with no destinations
|
||||||
|
THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination);
|
||||||
|
|
||||||
|
// calculate total amount being sent to all destinations
|
||||||
|
// throw if total amount overflows uint64_t
|
||||||
|
needed_money = 0;
|
||||||
|
BOOST_FOREACH(auto& dt, dsts)
|
||||||
|
{
|
||||||
|
THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination);
|
||||||
|
needed_money += dt.amount;
|
||||||
|
LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money));
|
||||||
|
THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, 0, m_testnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw if attempting a transaction with no money
|
||||||
|
THROW_WALLET_EXCEPTION_IF(needed_money == 0, error::zero_destination);
|
||||||
|
|
||||||
|
// gather all our dust and non dust outputs
|
||||||
|
for (size_t i = 0; i < m_transfers.size(); ++i)
|
||||||
|
{
|
||||||
|
const transfer_details& td = m_transfers[i];
|
||||||
|
if (!td.m_spent && is_transfer_unlocked(td))
|
||||||
|
{
|
||||||
|
if (::config::DEFAULT_DUST_THRESHOLD <= td.amount())
|
||||||
|
unused_transfers_indices.push_back(i);
|
||||||
|
else
|
||||||
|
unused_dust_indices.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs");
|
||||||
|
|
||||||
|
// start with an empty tx
|
||||||
|
txes.push_back(TX());
|
||||||
|
accumulated_fee = 0;
|
||||||
|
accumulated_outputs = 0;
|
||||||
|
accumulated_change = 0;
|
||||||
|
adding_fee = false;
|
||||||
|
needed_fee = 0;
|
||||||
|
|
||||||
|
// while we have something to send
|
||||||
|
while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee) {
|
||||||
|
TX &tx = txes.back();
|
||||||
|
|
||||||
|
// if we need to spend money and don't have any left, we fail
|
||||||
|
if (unused_dust_indices.empty() && unused_transfers_indices.empty()) {
|
||||||
|
LOG_PRINT_L2("No more outputs to choose from");
|
||||||
|
THROW_WALLET_EXCEPTION_IF(1, error::not_enough_money, unlocked_balance(), needed_money, accumulated_fee + needed_fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a random unspent output and use it to pay part (or all) of the current destination (and maybe next one, etc)
|
||||||
|
// This could be more clever, but maybe at the cost of making probabilistic inferences easier
|
||||||
|
size_t idx = !unused_transfers_indices.empty() ? pop_random_value(unused_transfers_indices) : pop_random_value(unused_dust_indices);
|
||||||
|
|
||||||
|
const transfer_details &td = m_transfers[idx];
|
||||||
|
LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount()));
|
||||||
|
|
||||||
|
// add this output to the list to spend
|
||||||
|
tx.selected_transfers.push_back(m_transfers.begin() + idx);
|
||||||
|
uint64_t available_amount = td.amount();
|
||||||
|
accumulated_outputs += available_amount;
|
||||||
|
|
||||||
|
if (adding_fee)
|
||||||
|
{
|
||||||
|
LOG_PRINT_L2("We need more fee, adding it to fee");
|
||||||
|
available_for_fee += available_amount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (!dsts.empty() && dsts[0].amount <= available_amount)
|
||||||
|
{
|
||||||
|
// we can fully pay that destination
|
||||||
|
LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_testnet, dsts[0].addr) <<
|
||||||
|
" for " << print_money(dsts[0].amount));
|
||||||
|
tx.add(dsts[0].addr, dsts[0].amount);
|
||||||
|
available_amount -= dsts[0].amount;
|
||||||
|
dsts[0].amount = 0;
|
||||||
|
pop_index(dsts, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (available_amount > 0 && !dsts.empty()) {
|
||||||
|
// we can partially fill that destination
|
||||||
|
LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_testnet, dsts[0].addr) <<
|
||||||
|
" for " << print_money(available_amount) << "/" << print_money(dsts[0].amount));
|
||||||
|
tx.add(dsts[0].addr, available_amount);
|
||||||
|
dsts[0].amount -= available_amount;
|
||||||
|
available_amount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// here, check if we need to sent tx and start a new one
|
||||||
|
LOG_PRINT_L2("Considering whether to create a tx now, " << tx.selected_transfers.size() << " inputs, tx limit "
|
||||||
|
<< m_upper_transaction_size_limit);
|
||||||
|
bool try_tx;
|
||||||
|
if (adding_fee)
|
||||||
|
{
|
||||||
|
/* might not actually be enough if adding this output bumps size to next kB, but we need to try */
|
||||||
|
try_tx = available_for_fee >= needed_fee;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try_tx = dsts.empty() || (tx.selected_transfers.size() * (fake_outs_count+1) * APPROXIMATE_INPUT_BYTES >= TX_SIZE_TARGET(m_upper_transaction_size_limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try_tx) {
|
||||||
|
cryptonote::transaction test_tx;
|
||||||
|
pending_tx test_ptx;
|
||||||
|
|
||||||
|
needed_fee = 0;
|
||||||
|
|
||||||
|
LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " destinations and " <<
|
||||||
|
tx.selected_transfers.size() << " outputs");
|
||||||
|
transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra,
|
||||||
|
detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx);
|
||||||
|
auto txBlob = t_serializable_object_to_blob(test_ptx.tx);
|
||||||
|
uint64_t txSize = txBlob.size();
|
||||||
|
uint64_t numKB = txSize / 1024;
|
||||||
|
if (txSize % 1024)
|
||||||
|
{
|
||||||
|
numKB++;
|
||||||
|
}
|
||||||
|
needed_fee = numKB * FEE_PER_KB;
|
||||||
|
available_for_fee = test_ptx.fee + test_ptx.change_dts.amount;
|
||||||
|
LOG_PRINT_L2("Made a " << numKB << " kB tx, with " << print_money(available_for_fee) << " available for fee (" <<
|
||||||
|
print_money(needed_fee) << " needed)");
|
||||||
|
|
||||||
|
if (needed_fee > available_for_fee && dsts[0].amount > 0)
|
||||||
|
{
|
||||||
|
// we don't have enough for the fee, but we've only partially paid the current address,
|
||||||
|
// so we can take the fee from the paid amount, since we'll have to make another tx anyway
|
||||||
|
std::vector<cryptonote::tx_destination_entry>::iterator i;
|
||||||
|
i = std::find_if(tx.dsts.begin(), tx.dsts.end(),
|
||||||
|
[&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &dsts[0].addr, sizeof(dsts[0].addr)); });
|
||||||
|
THROW_WALLET_EXCEPTION_IF(i == tx.dsts.end(), error::wallet_internal_error, "paid address not found in outputs");
|
||||||
|
if (i->amount > needed_fee)
|
||||||
|
{
|
||||||
|
uint64_t new_paid_amount = i->amount /*+ test_ptx.fee*/ - needed_fee;
|
||||||
|
LOG_PRINT_L2("Adjusting amount paid to " << get_account_address_as_str(m_testnet, i->addr) << " from " <<
|
||||||
|
print_money(i->amount) << " to " << print_money(new_paid_amount) << " to accomodate " <<
|
||||||
|
print_money(needed_fee) << " fee");
|
||||||
|
dsts[0].amount += i->amount - new_paid_amount;
|
||||||
|
i->amount = new_paid_amount;
|
||||||
|
test_ptx.fee = needed_fee;
|
||||||
|
available_for_fee = needed_fee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needed_fee > available_for_fee)
|
||||||
|
{
|
||||||
|
LOG_PRINT_L2("We could not make a tx, switching to fee accumulation");
|
||||||
|
|
||||||
|
adding_fee = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_PRINT_L2("We made a tx, adjusting fee and saving it");
|
||||||
|
transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra,
|
||||||
|
detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx);
|
||||||
|
txBlob = t_serializable_object_to_blob(test_ptx.tx);
|
||||||
|
LOG_PRINT_L2("Made a final " << ((txBlob.size() + 1023)/1024) << " kB tx, with " << print_money(test_ptx.fee) <<
|
||||||
|
" fee and " << print_money(test_ptx.change_dts.amount) << " change");
|
||||||
|
|
||||||
|
tx.tx = test_tx;
|
||||||
|
tx.ptx = test_ptx;
|
||||||
|
tx.bytes = txBlob.size();
|
||||||
|
accumulated_fee += test_ptx.fee;
|
||||||
|
accumulated_change += test_ptx.change_dts.amount;
|
||||||
|
adding_fee = false;
|
||||||
|
if (!dsts.empty())
|
||||||
|
{
|
||||||
|
LOG_PRINT_L2("We have more to pay, starting another tx");
|
||||||
|
txes.push_back(TX());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adding_fee)
|
||||||
|
{
|
||||||
|
LOG_PRINT_L1("We ran out of outputs while trying to gather final fee");
|
||||||
|
THROW_WALLET_EXCEPTION_IF(1, error::not_enough_money, unlocked_balance(), needed_money, accumulated_fee + needed_fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_PRINT_L1("Done creating " << txes.size() << " transactions, " << print_money(accumulated_fee) <<
|
||||||
|
" total fee, " << print_money(accumulated_change) << " total change");
|
||||||
|
|
||||||
|
std::vector<wallet2::pending_tx> ptx_vector;
|
||||||
|
for (std::vector<TX>::iterator i = txes.begin(); i != txes.end(); ++i)
|
||||||
|
{
|
||||||
|
TX &tx = *i;
|
||||||
|
uint64_t tx_money = 0;
|
||||||
|
for (std::list<transfer_container::iterator>::const_iterator mi = tx.selected_transfers.begin(); mi != tx.selected_transfers.end(); ++mi)
|
||||||
|
tx_money += (*mi)->amount();
|
||||||
|
LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() <<
|
||||||
|
": " << (tx.bytes+1023)/1024 << " kB, sending " << print_money(tx_money) << " in " << tx.selected_transfers.size() <<
|
||||||
|
" outputs to " << tx.dsts.size() << " destination(s), including " <<
|
||||||
|
print_money(tx.ptx.fee) << " fee, " << print_money(tx.ptx.change_dts.amount) << " change");
|
||||||
|
ptx_vector.push_back(tx.ptx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we made it this far, we're OK to actually send the transactions
|
||||||
|
return ptx_vector;
|
||||||
|
}
|
||||||
|
|
||||||
uint64_t wallet2::unlocked_dust_balance(const tx_dust_policy &dust_policy) const
|
uint64_t wallet2::unlocked_dust_balance(const tx_dust_policy &dust_policy) const
|
||||||
{
|
{
|
||||||
uint64_t money = 0;
|
uint64_t money = 0;
|
||||||
|
|
|
@ -221,9 +221,14 @@ namespace tools
|
||||||
void transfer(const std::vector<cryptonote::tx_destination_entry>& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx& ptx);
|
void transfer(const std::vector<cryptonote::tx_destination_entry>& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx& ptx);
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx);
|
void transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx);
|
||||||
|
template<typename T>
|
||||||
|
void transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count,
|
||||||
|
uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx);
|
||||||
|
|
||||||
void commit_tx(pending_tx& ptx_vector);
|
void commit_tx(pending_tx& ptx_vector);
|
||||||
void commit_tx(std::vector<pending_tx>& ptx_vector);
|
void commit_tx(std::vector<pending_tx>& ptx_vector);
|
||||||
std::vector<pending_tx> create_transactions(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee, const std::vector<uint8_t> extra);
|
std::vector<pending_tx> create_transactions(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee, const std::vector<uint8_t> extra);
|
||||||
|
std::vector<wallet2::pending_tx> create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector<uint8_t> extra);
|
||||||
std::vector<pending_tx> create_dust_sweep_transactions();
|
std::vector<pending_tx> create_dust_sweep_transactions();
|
||||||
bool check_connection();
|
bool check_connection();
|
||||||
void get_transfers(wallet2::transfer_container& incoming_transfers) const;
|
void get_transfers(wallet2::transfer_container& incoming_transfers) const;
|
||||||
|
|
|
@ -254,7 +254,11 @@ namespace tools
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::vector<wallet2::pending_tx> ptx_vector = m_wallet.create_transactions(dsts, req.mixin, req.unlock_time, req.fee, extra);
|
std::vector<wallet2::pending_tx> ptx_vector;
|
||||||
|
if (req.new_algorithm)
|
||||||
|
ptx_vector = m_wallet.create_transactions_2(dsts, req.mixin, req.unlock_time, req.fee, extra);
|
||||||
|
else
|
||||||
|
ptx_vector = m_wallet.create_transactions(dsts, req.mixin, req.unlock_time, req.fee, extra);
|
||||||
|
|
||||||
m_wallet.commit_tx(ptx_vector);
|
m_wallet.commit_tx(ptx_vector);
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,7 @@ namespace wallet_rpc
|
||||||
uint64_t mixin;
|
uint64_t mixin;
|
||||||
uint64_t unlock_time;
|
uint64_t unlock_time;
|
||||||
std::string payment_id;
|
std::string payment_id;
|
||||||
|
bool new_algorithm;
|
||||||
|
|
||||||
BEGIN_KV_SERIALIZE_MAP()
|
BEGIN_KV_SERIALIZE_MAP()
|
||||||
KV_SERIALIZE(destinations)
|
KV_SERIALIZE(destinations)
|
||||||
|
@ -133,6 +134,7 @@ namespace wallet_rpc
|
||||||
KV_SERIALIZE(mixin)
|
KV_SERIALIZE(mixin)
|
||||||
KV_SERIALIZE(unlock_time)
|
KV_SERIALIZE(unlock_time)
|
||||||
KV_SERIALIZE(payment_id)
|
KV_SERIALIZE(payment_id)
|
||||||
|
KV_SERIALIZE(new_algorithm)
|
||||||
END_KV_SERIALIZE_MAP()
|
END_KV_SERIALIZE_MAP()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue