diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/blocks/checkpoints.dat | bin | 373124 -> 385988 bytes | |||
-rw-r--r-- | src/checkpoints/checkpoints.cpp | 1 | ||||
-rw-r--r-- | src/cryptonote_core/blockchain.cpp | 5 | ||||
-rw-r--r-- | src/net/i2p_address.cpp | 45 | ||||
-rw-r--r-- | src/net/i2p_address.h | 9 | ||||
-rw-r--r-- | src/net/parse.cpp | 2 | ||||
-rw-r--r-- | src/p2p/net_peerlist_boost_serialization.h | 2 | ||||
-rw-r--r-- | src/ringct/rctTypes.h | 4 | ||||
-rw-r--r-- | src/simplewallet/simplewallet.cpp | 83 | ||||
-rw-r--r-- | src/version.cpp.in | 2 | ||||
-rw-r--r-- | src/wallet/wallet2.cpp | 228 | ||||
-rw-r--r-- | src/wallet/wallet2.h | 6 | ||||
-rw-r--r-- | src/wallet/wallet_errors.h | 10 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server.cpp | 22 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server.h | 4 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server_commands_defs.h | 19 |
16 files changed, 337 insertions, 105 deletions
diff --git a/src/blocks/checkpoints.dat b/src/blocks/checkpoints.dat Binary files differindex bb6f9b421..d304c30de 100644 --- a/src/blocks/checkpoints.dat +++ b/src/blocks/checkpoints.dat diff --git a/src/checkpoints/checkpoints.cpp b/src/checkpoints/checkpoints.cpp index fe1508e21..c8cfb1289 100644 --- a/src/checkpoints/checkpoints.cpp +++ b/src/checkpoints/checkpoints.cpp @@ -248,6 +248,7 @@ namespace cryptonote ADD_CHECKPOINT2(2851000, "5bf0e47fc782263191a33f63a67db6c711781dc2a3c442e17ed901ec401be5c9", "0x3b6cd8a8ed610e8"); ADD_CHECKPOINT2(2971000, "3d4cac5ac515eeabd18769ab943af85f36db51d28720def0d0e6effc2c8f5ce3", "0x436e532738b8b5b"); ADD_CHECKPOINT2(2985000, "08f5e6b7301c1b6ed88268a28f8677a06e8ff943b3f9e48d3080f71f9c134bfb", "0x444b7b42a633c96"); + ADD_CHECKPOINT2(3088000, "bddf8ca09110d33d6d497f13a113630c2b6af1c84d4f3a6f35cb1446f2604ade", "0x4aed3615c2f8c3e"); return true; } diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 9d8f0f4de..c29520a1e 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -3832,6 +3832,8 @@ void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_block const uint8_t version = get_current_hard_fork_version(); const uint64_t db_height = m_db->height(); + CHECK_AND_ASSERT_THROW_MES(grace_blocks <= CRYPTONOTE_REWARD_BLOCKS_WINDOW, "Grace blocks invalid In 2021 fee scaling estimate."); + // we want Mlw = median of max((min(Mbw, 1.7 * Ml), Zm), Ml / 1.7) // Mbw: block weight for the last 99990 blocks, 0 for the next 10 // Ml: penalty free zone (dynamic), aka long_term_median, aka median of max((min(Mb, 1.7 * Ml), Zm), Ml / 1.7) @@ -3845,7 +3847,6 @@ void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_block const uint64_t Mlw_penalty_free_zone_for_wallet = std::max<uint64_t>(rm.median(), CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5); // Msw: median over [100 - grace blocks] past + [grace blocks] future blocks - CHECK_AND_ASSERT_THROW_MES(grace_blocks <= 100, "Grace blocks invalid In 2021 fee scaling estimate."); std::vector<uint64_t> weights; get_last_n_blocks_weights(weights, 100 - grace_blocks); weights.reserve(100); @@ -5551,7 +5552,7 @@ void Blockchain::cancel() } #if defined(PER_BLOCK_CHECKPOINT) -static const char expected_block_hashes_hash[] = "bc9c91329af96137390d9c709fa3cecc924f1b25dadb7589f0d751cd93f3cc39"; +static const char expected_block_hashes_hash[] = "374d836b26c46b9d8fdbc977da7a89a43c2eefb65659901ee4d0053d302e1468"; void Blockchain::load_compiled_in_block_hashes(const GetCheckpointsCallback& get_checkpoints) { if (get_checkpoints == nullptr || !m_fast_sync) diff --git a/src/net/i2p_address.cpp b/src/net/i2p_address.cpp index b0194a525..4e21085d0 100644 --- a/src/net/i2p_address.cpp +++ b/src/net/i2p_address.cpp @@ -71,7 +71,7 @@ namespace net struct i2p_serialized { std::string host; - std::uint16_t port; + std::uint16_t port; //! Leave for compatability with older clients BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(host) @@ -80,8 +80,7 @@ namespace net }; } - i2p_address::i2p_address(const boost::string_ref host, const std::uint16_t port) noexcept - : port_(port) + i2p_address::i2p_address(const boost::string_ref host) noexcept { // this is a private constructor, throw if moved to public assert(host.size() < sizeof(host_)); @@ -97,27 +96,19 @@ namespace net } i2p_address::i2p_address() noexcept - : port_(0) { static_assert(sizeof(unknown_host) <= sizeof(host_), "bad buffer size"); std::memcpy(host_, unknown_host, sizeof(unknown_host)); std::memset(host_ + sizeof(unknown_host), 0, sizeof(host_) - sizeof(unknown_host)); } - expect<i2p_address> i2p_address::make(const boost::string_ref address, const std::uint16_t default_port) + expect<i2p_address> i2p_address::make(const boost::string_ref address) { boost::string_ref host = address.substr(0, address.rfind(':')); - const boost::string_ref port = - address.substr(host.size() + (host.size() == address.size() ? 0 : 1)); - MONERO_CHECK(host_check(host)); - std::uint16_t porti = default_port; - if (!port.empty() && !epee::string_tools::get_xtype_from_string(porti, std::string{port})) - return {net::error::invalid_port}; - static_assert(b32_length + sizeof(tld) == sizeof(i2p_address::host_), "bad internal host size"); - return i2p_address{host, porti}; + return i2p_address{host}; } bool i2p_address::_load(epee::serialization::portable_storage& src, epee::serialization::section* hparent) @@ -127,23 +118,21 @@ namespace net { std::memcpy(host_, in.host.data(), in.host.size()); std::memset(host_ + in.host.size(), 0, sizeof(host_) - in.host.size()); - port_ = in.port; return true; } static_assert(sizeof(unknown_host) <= sizeof(host_), "bad buffer size"); std::memcpy(host_, unknown_host, sizeof(unknown_host)); // include null terminator - port_ = 0; return false; } bool i2p_address::store(epee::serialization::portable_storage& dest, epee::serialization::section* hparent) const { - const i2p_serialized out{std::string{host_}, port_}; + // Set port to 1 for backwards compatability; zero is invalid port + const i2p_serialized out{std::string{host_}, 1}; return out.store(dest, hparent); } i2p_address::i2p_address(const i2p_address& rhs) noexcept - : port_(rhs.port_) { std::memcpy(host_, rhs.host_, sizeof(host_)); } @@ -152,7 +141,6 @@ namespace net { if (this != std::addressof(rhs)) { - port_ = rhs.port_; std::memcpy(host_, rhs.host_, sizeof(host_)); } return *this; @@ -166,13 +154,12 @@ namespace net bool i2p_address::equal(const i2p_address& rhs) const noexcept { - return port_ == rhs.port_ && is_same_host(rhs); + return is_same_host(rhs); } bool i2p_address::less(const i2p_address& rhs) const noexcept { - int res = std::strcmp(host_str(), rhs.host_str()); - return res < 0 || (res == 0 && port() < rhs.port()); + return std::strcmp(host_str(), rhs.host_str()) < 0; } bool i2p_address::is_same_host(const i2p_address& rhs) const noexcept @@ -182,20 +169,6 @@ namespace net std::string i2p_address::str() const { - const std::size_t host_length = std::strlen(host_str()); - const std::size_t port_length = - port_ == 0 ? 0 : std::numeric_limits<std::uint16_t>::digits10 + 2; - - std::string out{}; - out.reserve(host_length + port_length); - out.assign(host_str(), host_length); - - if (port_ != 0) - { - out.push_back(':'); - namespace karma = boost::spirit::karma; - karma::generate(std::back_inserter(out), karma::ushort_, port()); - } - return out; + return host_str(); } } diff --git a/src/net/i2p_address.h b/src/net/i2p_address.h index 7f6ef1b3f..92579399f 100644 --- a/src/net/i2p_address.h +++ b/src/net/i2p_address.h @@ -50,11 +50,10 @@ namespace net //! b32 i2p address; internal format not condensed/decoded. class i2p_address { - std::uint16_t port_; char host_[61]; // null-terminated //! Keep in private, `host.size()` has no runtime check - i2p_address(boost::string_ref host, std::uint16_t port) noexcept; + i2p_address(boost::string_ref host) noexcept; public: //! \return Size of internal buffer for host. @@ -74,7 +73,7 @@ namespace net with `default_port` being used if port is not specified in `address`. */ - static expect<i2p_address> make(boost::string_ref address, std::uint16_t default_port = 0); + static expect<i2p_address> make(boost::string_ref address); //! Load from epee p2p format, and \return false if not valid tor address bool _load(epee::serialization::portable_storage& src, epee::serialization::section* hparent); @@ -103,8 +102,8 @@ namespace net //! \return Null-terminated `x.b32.i2p` value or `unknown_str()`. const char* host_str() const noexcept { return host_; } - //! \return Port value or `0` if unspecified. - std::uint16_t port() const noexcept { return port_; } + //! \return `1` to work with I2P socks which considers `0` error. + std::uint16_t port() const noexcept { return 1; } static constexpr bool is_loopback() noexcept { return false; } static constexpr bool is_local() noexcept { return false; } diff --git a/src/net/parse.cpp b/src/net/parse.cpp index 92be492a3..b76aefad7 100644 --- a/src/net/parse.cpp +++ b/src/net/parse.cpp @@ -81,7 +81,7 @@ namespace net if (host_str_ref.ends_with(".onion")) return tor_address::make(address, default_port); if (host_str_ref.ends_with(".i2p")) - return i2p_address::make(address, default_port); + return i2p_address::make(address); boost::system::error_code ec; boost::asio::ip::address_v6 v6 = boost::asio::ip::address_v6::from_string(host_str, ec); diff --git a/src/p2p/net_peerlist_boost_serialization.h b/src/p2p/net_peerlist_boost_serialization.h index 7d8bd1480..100fe06ca 100644 --- a/src/p2p/net_peerlist_boost_serialization.h +++ b/src/p2p/net_peerlist_boost_serialization.h @@ -196,7 +196,7 @@ namespace boost if (std::strcmp(host, net::i2p_address::unknown_str()) == 0) na = net::i2p_address::unknown(); else - na = MONERO_UNWRAP(net::i2p_address::make(host, port)); + na = MONERO_UNWRAP(net::i2p_address::make(host)); } template <class Archive, class ver_type> diff --git a/src/ringct/rctTypes.h b/src/ringct/rctTypes.h index 32cd8dc6f..dc3d9768b 100644 --- a/src/ringct/rctTypes.h +++ b/src/ringct/rctTypes.h @@ -325,6 +325,10 @@ namespace rct { ctkeyV outPk; xmr_amount txnFee; // contains b + rctSigBase() : + type(RCTTypeNull), message{}, mixRing{}, pseudoOuts{}, ecdhInfo{}, outPk{}, txnFee(0) + {} + template<bool W, template <bool> class Archive> bool serialize_rctsig_base(Archive<W> &ar, size_t inputs, size_t outputs) { diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 18cd0c2a6..7372d8d15 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -196,7 +196,7 @@ namespace const char* USAGE_INCOMING_TRANSFERS("incoming_transfers [available|unavailable] [verbose] [uses] [index=<N1>[,<N2>[,...]]]"); const char* USAGE_PAYMENTS("payments <PID_1> [<PID_2> ... <PID_N>]"); const char* USAGE_PAYMENT_ID("payment_id"); - const char* USAGE_TRANSFER("transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] (<URI> | <address> <amount>) [<payment_id>]"); + const char* USAGE_TRANSFER("transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] (<URI> | <address> <amount>) [subtractfeefrom=<D0>[,<D1>,all,...]] [<payment_id>]"); const char* USAGE_LOCKED_TRANSFER("locked_transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] (<URI> | <addr> <amount>) <lockblocks> [<payment_id (obsolete)>]"); const char* USAGE_LOCKED_SWEEP_ALL("locked_sweep_all [index=<N1>[,<N2>,...] | index=all] [<priority>] [<ring_size>] <address> <lockblocks> [<payment_id (obsolete)>]"); const char* USAGE_SWEEP_ALL("sweep_all [index=<N1>[,<N2>,...] | index=all] [<priority>] [<ring_size>] [outputs=<N>] <address> [<payment_id (obsolete)>]"); @@ -531,7 +531,52 @@ namespace fail_msg_writer() << sw::tr("invalid format for subaddress lookahead; must be <major>:<minor>"); return r; } -} + + static constexpr const char* SFFD_ARG_NAME{"subtractfeefrom="}; + + bool parse_subtract_fee_from_outputs + ( + const std::string& arg, + tools::wallet2::unique_index_container& subtract_fee_from_outputs, + bool& subtract_fee_from_all, + bool& matches + ) + { + matches = false; + if (!boost::string_ref{arg}.starts_with(SFFD_ARG_NAME)) // if arg doesn't match + return true; + matches = true; + + const char* arg_end = arg.c_str() + arg.size(); + for (const char* p = arg.c_str() + strlen(SFFD_ARG_NAME); p < arg_end;) + { + const char* new_p = nullptr; + const unsigned long dest_index = strtoul(p, const_cast<char**>(&new_p), 10); + if (dest_index == 0 && new_p == p) // numerical conversion failed + { + if (0 != strncmp(p, "all", 3)) + { + fail_msg_writer() << tr("Failed to parse subtractfeefrom list"); + return false; + } + subtract_fee_from_all = true; + break; + } + else if (dest_index > std::numeric_limits<uint32_t>::max()) + { + fail_msg_writer() << tr("Destination index is too large") << ": " << dest_index; + return false; + } + else + { + subtract_fee_from_outputs.insert(dest_index); + p = new_p + 1; // skip the comma + } + } + + return true; + } +} // anonymous namespace void simple_wallet::handle_transfer_exception(const std::exception_ptr &e, bool trusted_daemon) { @@ -3290,7 +3335,7 @@ simple_wallet::simple_wallet() tr("Show the blockchain height.")); m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::on_command, this, &simple_wallet::transfer, _1), tr(USAGE_TRANSFER), - tr("Transfer <amount> to <address>. If the parameter \"index=<N1>[,<N2>,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. <priority> is the priority of the transaction. The higher the priority, the higher the transaction fee. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. <ring_size> is the number of inputs to include for untraceability. Multiple payments can be made at once by adding URI_2 or <address_2> <amount_2> etcetera (before the payment ID, if it's included)")); + tr("Transfer <amount> to <address>. If the parameter \"index=<N1>[,<N2>,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. <priority> is the priority of the transaction. The higher the priority, the higher the transaction fee. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. <ring_size> is the number of inputs to include for untraceability. Multiple payments can be made at once by adding URI_2 or <address_2> <amount_2> etcetera (before the payment ID, if it's included). The \"subtractfeefrom=\" list allows you to choose which destinations to fund the tx fee from instead of the change output. The fee will be split across the chosen destinations proportionally equally. For example, to make 3 transfers where the fee is taken from the first and third destinations, one could do: \"transfer <addr1> 3 <addr2> 0.5 <addr3> 1 subtractfeefrom=0,2\". Let's say the tx fee is 0.1. The balance would drop by exactly 4.5 XMR including fees, and addr1 & addr3 would receive 2.925 & 0.975 XMR, respectively. Use \"subtractfeefrom=all\" to spread the fee across all destinations.")); m_cmd_binder.set_handler("locked_transfer", boost::bind(&simple_wallet::on_command, this, &simple_wallet::locked_transfer,_1), tr(USAGE_LOCKED_TRANSFER), @@ -6640,6 +6685,27 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri local_args.pop_back(); } + // Parse subtractfeefrom destination list + tools::wallet2::unique_index_container subtract_fee_from_outputs; + bool subtract_fee_from_all = false; + for (auto it = local_args.begin(); it < local_args.end();) + { + bool matches = false; + if (!parse_subtract_fee_from_outputs(*it, subtract_fee_from_outputs, subtract_fee_from_all, matches)) + { + return false; + } + else if (matches) + { + it = local_args.erase(it); + break; + } + else + { + ++it; + } + } + vector<cryptonote::address_parse_info> dsts_info; vector<cryptonote::tx_destination_entry> dsts; for (size_t i = 0; i < local_args.size(); ) @@ -6736,6 +6802,13 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri dsts.push_back(de); } + if (subtract_fee_from_all) + { + subtract_fee_from_outputs.clear(); + for (decltype(subtract_fee_from_outputs)::value_type i = 0; i < dsts.size(); ++i) + subtract_fee_from_outputs.insert(i); + } + SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); try @@ -6754,13 +6827,13 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri return false; } unlock_block = bc_height + locked_blocks; - ptx_vector = m_wallet->create_transactions_2(dsts, fake_outs_count, unlock_block /* unlock_time */, priority, extra, m_current_subaddress_account, subaddr_indices); + ptx_vector = m_wallet->create_transactions_2(dsts, fake_outs_count, unlock_block /* unlock_time */, priority, extra, m_current_subaddress_account, subaddr_indices, subtract_fee_from_outputs); break; default: LOG_ERROR("Unknown transfer method, using default"); /* FALLTHRU */ case Transfer: - ptx_vector = m_wallet->create_transactions_2(dsts, fake_outs_count, 0 /* unlock_time */, priority, extra, m_current_subaddress_account, subaddr_indices); + ptx_vector = m_wallet->create_transactions_2(dsts, fake_outs_count, 0 /* unlock_time */, priority, extra, m_current_subaddress_account, subaddr_indices, subtract_fee_from_outputs); break; } diff --git a/src/version.cpp.in b/src/version.cpp.in index c93430003..f4bb1779f 100644 --- a/src/version.cpp.in +++ b/src/version.cpp.in @@ -1,5 +1,5 @@ #define DEF_MONERO_VERSION_TAG "@VERSIONTAG@" -#define DEF_MONERO_VERSION "0.18.3.1" +#define DEF_MONERO_VERSION "0.18.3.2" #define DEF_MONERO_RELEASE_NAME "Fluorine Fermi" #define DEF_MONERO_VERSION_FULL DEF_MONERO_VERSION "-" DEF_MONERO_VERSION_TAG #define DEF_MONERO_VERSION_IS_RELEASE @VERSION_IS_RELEASE@ diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 0218c8292..b48af7721 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -4324,7 +4324,10 @@ void wallet2::handle_reorg(uint64_t height, std::map<std::pair<uint64_t, uint64_ // C THROW_WALLET_EXCEPTION_IF(height < m_blockchain.offset() && m_blockchain.size() > m_blockchain.offset(), error::wallet_internal_error, "Daemon claims reorg below last checkpoint"); - detach_blockchain(height, output_tracker_cache); + detached_blockchain_data dbd = detach_blockchain(height, output_tracker_cache); + + if (m_callback) + m_callback->on_reorg(height, dbd.detached_blockchain.size(), dbd.detached_tx_hashes.size()); } //---------------------------------------------------------------------------------------------------- bool wallet2::deinit() @@ -8909,6 +8912,26 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> COMMAND_RPC_GET_OUTPUTS_BIN::request req = AUTO_VAL_INIT(req); COMMAND_RPC_GET_OUTPUTS_BIN::response daemon_resp = AUTO_VAL_INIT(daemon_resp); + // The secret picking order contains outputs in the order that we selected them. + // + // We will later sort the output request entries in a pre-determined order so that the daemon + // that we're requesting information from doesn't learn any information about the true spend + // for each ring. However, internally, we want to prefer to construct our rings using the + // outputs that we picked first versus outputs picked later. + // + // The reason why is because each consecutive output pick within a ring becomes increasing less + // statistically independent from other picks, since we pick outputs from a finite set + // *without replacement*, due to the protocol not allowing duplicate ring members. This effect + // is exacerbated by the fact that we pick 1.5x + 75 as many outputs as we need per RPC + // request to account for unusable outputs. This effect is small, but non-neglibile and gets + // worse with larger ring sizes. + std::vector<get_outputs_out> secret_picking_order; + + // Convenience/safety lambda to make sure that both output lists req.outputs and secret_picking_order are updated together + // Each ring section of req.outputs gets sorted later after selecting all outputs for that ring + const auto add_output_to_lists = [&req, &secret_picking_order](const get_outputs_out &goo) + { req.outputs.push_back(goo); secret_picking_order.push_back(goo); }; + std::unique_ptr<gamma_picker> gamma; if (has_rct) gamma.reset(new gamma_picker(rct_offsets)); @@ -9043,7 +9066,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> if (out < num_outs) { MINFO("Using it"); - req.outputs.push_back({amount, out}); + add_output_to_lists({amount, out}); ++num_found; seen_indices.emplace(out); if (out == td.m_global_output_index) @@ -9065,12 +9088,12 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> if (num_outs <= requested_outputs_count) { for (uint64_t i = 0; i < num_outs; i++) - req.outputs.push_back({amount, i}); + add_output_to_lists({amount, i}); // duplicate to make up shortfall: this will be caught after the RPC call, // so we can also output the amounts for which we can't reach the required // mixin after checking the actual unlockedness for (uint64_t i = num_outs; i < requested_outputs_count; ++i) - req.outputs.push_back({amount, num_outs - 1}); + add_output_to_lists({amount, num_outs - 1}); } else { @@ -9079,7 +9102,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> { num_found = 1; seen_indices.emplace(td.m_global_output_index); - req.outputs.push_back({amount, td.m_global_output_index}); + add_output_to_lists({amount, td.m_global_output_index}); LOG_PRINT_L1("Selecting real output: " << td.m_global_output_index << " for " << print_money(amount)); } @@ -9187,7 +9210,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> seen_indices.emplace(i); picks[type].insert(i); - req.outputs.push_back({amount, i}); + add_output_to_lists({amount, i}); ++num_found; MDEBUG("picked " << i << ", " << num_found << " now picked"); } @@ -9201,7 +9224,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> // we'll error out later while (num_found < requested_outputs_count) { - req.outputs.push_back({amount, 0}); + add_output_to_lists({amount, 0}); ++num_found; } } @@ -9211,6 +9234,10 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> [](const get_outputs_out &a, const get_outputs_out &b) { return a.index < b.index; }); } + THROW_WALLET_EXCEPTION_IF(req.outputs.size() != secret_picking_order.size(), error::wallet_internal_error, + "bug: we did not update req.outputs/secret_picking_order in tandem"); + + // List all requested outputs to debug log if (ELPP->vRegistry()->allowed(el::Level::Debug, MONERO_DEFAULT_LOG_CATEGORY)) { std::map<uint64_t, std::set<uint64_t>> outs; @@ -9331,18 +9358,21 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> } } - // then pick others in random order till we reach the required number - // since we use an equiprobable pick here, we don't upset the triangular distribution - std::vector<size_t> order; - order.resize(requested_outputs_count); - for (size_t n = 0; n < order.size(); ++n) - order[n] = n; - std::shuffle(order.begin(), order.end(), crypto::random_device{}); - + // While we are still lacking outputs in this result ring, in our secret pick order... LOG_PRINT_L2("Looking for " << (fake_outputs_count+1) << " outputs of size " << print_money(td.is_rct() ? 0 : td.amount())); - for (size_t o = 0; o < requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) + for (size_t ring_pick_idx = base; ring_pick_idx < base + requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++ring_pick_idx) { - size_t i = base + order[o]; + const get_outputs_out attempted_output = secret_picking_order[ring_pick_idx]; + + // Find the index i of our pick in the request/response arrays + size_t i; + for (i = base; i < base + requested_outputs_count; ++i) + if (req.outputs[i].index == attempted_output.index) + break; + THROW_WALLET_EXCEPTION_IF(i == base + requested_outputs_count, error::wallet_internal_error, + "Could not find index of picked output in requested outputs"); + + // Try adding this output's information to result ring if output isn't invalid LOG_PRINT_L2("Index " << i << "/" << requested_outputs_count << ": idx " << req.outputs[i].index << " (real " << td.m_global_output_index << "), unlocked " << daemon_resp.outs[i].unlocked << ", key " << daemon_resp.outs[i].key); tx_add_fake_output(outs, req.outputs[i].index, daemon_resp.outs[i].key, daemon_resp.outs[i].mask, td.m_global_output_index, daemon_resp.outs[i].unlocked, valid_public_keys_cache); } @@ -10523,7 +10553,7 @@ bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image, // 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, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices) +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, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const unique_index_container& subtract_fee_from_outputs) { //ensure device is let in NONE mode in any case hw::device &hwdev = m_account.get_device(); @@ -10538,11 +10568,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp } std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_transfers_indices_per_subaddr; std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_dust_indices_per_subaddr; - uint64_t needed_money; + uint64_t needed_money, total_needed_money; // 'needed_money' is the sum of the destination amounts, while 'total_needed_money' includes 'needed_money' plus the fee if not 'subtract_fee_from_outputs' uint64_t accumulated_fee, accumulated_change; struct TX { std::vector<size_t> selected_transfers; std::vector<cryptonote::tx_destination_entry> dsts; + std::vector<bool> dsts_are_fee_subtractable; cryptonote::transaction tx; pending_tx ptx; size_t weight; @@ -10552,9 +10583,11 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp TX() : weight(0), needed_fee(0) {} /* Add an output to the transaction. + * If merge_destinations is true, when adding a destination with an existing address, to increment the amount of the existing tx output instead of creating a new one + * If subtracting_fee is true, when we generate a final list of destinations for transfer_selected[_rct], this destination will be used to fund the tx fee * Returns True if the output was added, False if there are no more available output slots. */ - bool add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations, size_t max_dsts) { + bool add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations, size_t max_dsts, bool subtracting_fee) { if (merge_destinations) { std::vector<cryptonote::tx_destination_entry>::iterator i; @@ -10564,6 +10597,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp if (dsts.size() >= max_dsts) return false; dsts.push_back(de); + dsts_are_fee_subtractable.push_back(subtracting_fee); i = dsts.end() - 1; i->amount = 0; } @@ -10579,13 +10613,67 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp return false; dsts.push_back(de); dsts.back().amount = 0; + dsts_are_fee_subtractable.push_back(subtracting_fee); } THROW_WALLET_EXCEPTION_IF(memcmp(&dsts[original_output_index].addr, &de.addr, sizeof(de.addr)), error::wallet_internal_error, "Mismatched destination address"); dsts[original_output_index].amount += amount; } return true; } + + // Returns destinations adjusted for given fee if subtract_fee_from_outputs is enabled + std::vector<cryptonote::tx_destination_entry> get_adjusted_dsts(uint64_t needed_fee) const + { + uint64_t dest_total = 0; + uint64_t subtractable_dest_total = 0; + std::vector<size_t> subtractable_indices; + subtractable_indices.reserve(dsts.size()); + for (size_t i = 0; i < dsts.size(); ++i) + { + dest_total += dsts[i].amount; + if (dsts_are_fee_subtractable[i]) + { + subtractable_dest_total += dsts[i].amount; + subtractable_indices.push_back(i); + } + } + + if (subtractable_indices.empty()) // if subtract_fee_from_outputs is not enabled for this tx + return dsts; + + THROW_WALLET_EXCEPTION_IF(subtractable_dest_total < needed_fee, error::tx_not_possible, + subtractable_dest_total, dest_total, needed_fee); + + std::vector<cryptonote::tx_destination_entry> res = dsts; + + // subtract fees from destinations equally, rounded down, until dust is left where we subtract 1 + uint64_t subtractable_remaining = needed_fee; + auto si_it = subtractable_indices.cbegin(); + uint64_t amount_to_subtract = 0; + while (subtractable_remaining) + { + // Set the amount to subtract iterating at the beginning of the list so equal amounts are + // subtracted throughout the list of destinations. We use max(x, 1) so that we we still step + // forwards even when the amount remaining is less than the number of subtractable indices + if (si_it == subtractable_indices.cbegin()) + amount_to_subtract = std::max<uint64_t>(subtractable_remaining / subtractable_indices.size(), 1); + + cryptonote::tx_destination_entry& d = res[*si_it]; + THROW_WALLET_EXCEPTION_IF(d.amount <= amount_to_subtract, error::zero_amount); + + subtractable_remaining -= amount_to_subtract; + d.amount -= amount_to_subtract; + ++si_it; + + // Wrap around to first subtractable index once we hit the end of the list + if (si_it == subtractable_indices.cend()) + si_it = subtractable_indices.cbegin(); + } + + return res; + } }; + 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; @@ -10608,6 +10696,14 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); + // throw if subtract_fee_from_outputs has a bad index + THROW_WALLET_EXCEPTION_IF(subtract_fee_from_outputs.size() && *subtract_fee_from_outputs.crbegin() >= dsts.size(), + error::subtract_fee_from_bad_index, *subtract_fee_from_outputs.crbegin()); + + // throw if subtract_fee_from_outputs is enabled and we have too many outputs to fit into one tx + THROW_WALLET_EXCEPTION_IF(subtract_fee_from_outputs.size() && dsts.size() > BULLETPROOF_MAX_OUTPUTS - 1, + error::wallet_internal_error, "subtractfeefrom transfers cannot be split over multiple transactions yet"); + // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t needed_money = 0; @@ -10635,6 +10731,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // we could also check for being within FEE_PER_KB, but if the fee calculation // ever changes, this might be missed, so let this go through const uint64_t min_fee = (base_fee * estimate_tx_size(use_rct, 1, fake_outs_count, 2, extra.size(), bulletproof, clsag, bulletproof_plus, use_view_tags)); + total_needed_money = needed_money + (subtract_fee_from_outputs.size() ? 0 : min_fee); uint64_t balance_subtotal = 0; uint64_t unlocked_balance_subtotal = 0; for (uint32_t index_minor : subaddr_indices) @@ -10642,10 +10739,10 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp balance_subtotal += balance_per_subaddr[index_minor]; unlocked_balance_subtotal += unlocked_balance_per_subaddr[index_minor].first; } - THROW_WALLET_EXCEPTION_IF(needed_money + min_fee > balance_subtotal, error::not_enough_money, + THROW_WALLET_EXCEPTION_IF(total_needed_money > balance_subtotal || min_fee > balance_subtotal, error::not_enough_money, balance_subtotal, needed_money, 0); // first check overall balance is enough, then unlocked one, so we throw distinct exceptions - THROW_WALLET_EXCEPTION_IF(needed_money + min_fee > unlocked_balance_subtotal, error::not_enough_unlocked_money, + THROW_WALLET_EXCEPTION_IF(total_needed_money > unlocked_balance_subtotal || min_fee > unlocked_balance_subtotal, error::not_enough_unlocked_money, unlocked_balance_subtotal, needed_money, 0); for (uint32_t i : subaddr_indices) @@ -10748,7 +10845,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // this is used to build a tx that's 1 or 2 inputs, and 2 outputs, which // will get us a known fee. uint64_t estimated_fee = estimate_fee(use_per_byte_fee, use_rct, 2, fake_outs_count, 2, extra.size(), bulletproof, clsag, bulletproof_plus, use_view_tags, base_fee, fee_quantization_mask); - preferred_inputs = pick_preferred_rct_inputs(needed_money + estimated_fee, subaddr_account, subaddr_indices); + total_needed_money = needed_money + (subtract_fee_from_outputs.size() ? 0 : estimated_fee); + preferred_inputs = pick_preferred_rct_inputs(total_needed_money, subaddr_account, subaddr_indices); if (!preferred_inputs.empty()) { string s; @@ -10781,7 +10879,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // - we have something to send // - or we need to gather more fee // - or we have just one input in that tx, which is rct (to try and make all/most rct txes 2/2) - unsigned int original_output_index = 0; + unsigned int original_output_index = 0, destination_index = 0; std::vector<size_t>* unused_transfers_indices = &unused_transfers_indices_per_subaddr[0].second; std::vector<size_t>* unused_dust_indices = &unused_dust_indices_per_subaddr[0].second; @@ -10864,7 +10962,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // we can fully pay that destination LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(dsts[0].amount)); - if (!tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1)) + const bool subtract_fee_from_this_dest = subtract_fee_from_outputs.count(destination_index); + if (!tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1, subtract_fee_from_this_dest)) { LOG_PRINT_L2("Didn't pay: ran out of output slots"); out_slots_exhausted = true; @@ -10874,6 +10973,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp dsts[0].amount = 0; pop_index(dsts, 0); ++original_output_index; + ++destination_index; } if (!out_slots_exhausted && available_amount > 0 && !dsts.empty() && @@ -10881,7 +10981,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // we can partially fill that destination LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(available_amount) << "/" << print_money(dsts[0].amount)); - if (tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1)) + const bool subtract_fee_from_this_dest = subtract_fee_from_outputs.count(destination_index); + if (tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1, subtract_fee_from_this_dest)) { dsts[0].amount -= available_amount; available_amount = 0; @@ -10960,9 +11061,13 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // Try to carve the estimated fee from the partial payment (if there is one) available_for_fee = try_carving_from_partial_payment(needed_fee, available_for_fee); - uint64_t inputs = 0, outputs = needed_fee; + uint64_t inputs = 0, outputs = 0; for (size_t idx: tx.selected_transfers) inputs += m_transfers[idx].amount(); for (const auto &o: tx.dsts) outputs += o.amount; + if (subtract_fee_from_outputs.empty()) // if normal tx that doesn't subtract fees + { + outputs += needed_fee; + } if (inputs < outputs) { @@ -10973,15 +11078,32 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " outputs and " << tx.selected_transfers.size() << " inputs"); + auto tx_dsts = tx.get_adjusted_dsts(needed_fee); if (use_rct) - transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, + transfer_selected_rct(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, test_tx, test_ptx, rct_config, use_view_tags); else - transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, + transfer_selected(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx, use_view_tags); auto txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_quantization_mask); - available_for_fee = test_ptx.fee + test_ptx.change_dts.amount + (!test_ptx.dust_added_to_fee ? test_ptx.dust : 0); + + // Depending on the mode, we take extra fees from either our change output or the destination outputs for which subtract_fee_from_outputs is true + uint64_t output_available_for_fee = 0; + bool tx_has_subtractable_output = false; + for (size_t di = 0; di < tx.dsts.size(); ++di) + { + if (tx.dsts_are_fee_subtractable[di]) + { + output_available_for_fee += tx.dsts[di].amount; + tx_has_subtractable_output = true; + } + } + if (!tx_has_subtractable_output) + { + output_available_for_fee = test_ptx.change_dts.amount; + } + available_for_fee = test_ptx.fee + output_available_for_fee + (!test_ptx.dust_added_to_fee ? test_ptx.dust : 0); LOG_PRINT_L2("Made a " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); @@ -10997,18 +11119,24 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp else { LOG_PRINT_L2("We made a tx, adjusting fee and saving it, we need " << print_money(needed_fee) << " and we have " << print_money(test_ptx.fee)); - do { + size_t fee_tries; + for (fee_tries = 0; fee_tries < 10 && needed_fee > test_ptx.fee; ++fee_tries) { + tx_dsts = tx.get_adjusted_dsts(needed_fee); + if (use_rct) - transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, + transfer_selected_rct(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, test_tx, test_ptx, rct_config, use_view_tags); else - transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, + transfer_selected(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx, use_view_tags); txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_quantization_mask); LOG_PRINT_L2("Made an attempt at a final " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); - } while (needed_fee > test_ptx.fee); + }; + + THROW_WALLET_EXCEPTION_IF(fee_tries == 10, error::wallet_internal_error, + "Too many attempts to raise pending tx fee to level of needed fee"); LOG_PRINT_L2("Made a final " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); @@ -11061,10 +11189,13 @@ skip_tx: for (std::vector<TX>::iterator i = txes.begin(); i != txes.end(); ++i) { TX &tx = *i; + + const auto tx_dsts = tx.get_adjusted_dsts(tx.needed_fee); + cryptonote::transaction test_tx; pending_tx test_ptx; if (use_rct) { - transfer_selected_rct(tx.dsts, /* NOMOD std::vector<cryptonote::tx_destination_entry> dsts,*/ + transfer_selected_rct(tx_dsts, /* NOMOD std::vector<cryptonote::tx_destination_entry> dsts,*/ tx.selected_transfers, /* const std::list<size_t> selected_transfers */ fake_outs_count, /* CONST size_t fake_outputs_count, */ tx.outs, /* MOD std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, */ @@ -11077,7 +11208,7 @@ skip_tx: rct_config, use_view_tags); /* const bool use_view_tags */ } else { - transfer_selected(tx.dsts, + transfer_selected(tx_dsts, tx.selected_transfers, fake_outs_count, tx.outs, @@ -11111,23 +11242,38 @@ skip_tx: ptx_vector.push_back(tx.ptx); } - THROW_WALLET_EXCEPTION_IF(!sanity_check(ptx_vector, original_dsts), error::wallet_internal_error, "Created transaction(s) failed sanity check"); + THROW_WALLET_EXCEPTION_IF(!sanity_check(ptx_vector, original_dsts, subtract_fee_from_outputs), error::wallet_internal_error, "Created transaction(s) failed sanity check"); // if we made it this far, we're OK to actually send the transactions return ptx_vector; } -bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, std::vector<cryptonote::tx_destination_entry> dsts) const +bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, const std::vector<cryptonote::tx_destination_entry>& dsts, const unique_index_container& subtract_fee_from_outputs) const { - MDEBUG("sanity_check: " << ptx_vector.size() << " txes, " << dsts.size() << " destinations"); + MDEBUG("sanity_check: " << ptx_vector.size() << " txes, " << dsts.size() << " destinations, subtract_fee_from_outputs " << + (subtract_fee_from_outputs.size() ? "enabled" : "disabled")); THROW_WALLET_EXCEPTION_IF(ptx_vector.empty(), error::wallet_internal_error, "No transactions"); + THROW_WALLET_EXCEPTION_IF(!subtract_fee_from_outputs.empty() && ptx_vector.size() != 1, + error::wallet_internal_error, "feature subtractfeefrom not supported for split transactions"); + + // For destinations from where the fee is subtracted, the required amount has to be at least + // target amount - (tx fee / num_subtractable + 1). +1 since fee might not be evenly divisble by + // the number of subtractble destinations. For non-subtractable destinations, we need at least + // the target amount. + const size_t num_subtractable_dests = subtract_fee_from_outputs.size(); + const uint64_t fee0 = ptx_vector[0].fee; + const uint64_t subtractable_fee_deduction = fee0 / std::max<size_t>(num_subtractable_dests, 1) + 1; // check every party in there does receive at least the required amount std::unordered_map<account_public_address, std::pair<uint64_t, bool>> required; - for (const auto &d: dsts) + for (size_t i = 0; i < dsts.size(); ++i) { - required[d.addr].first += d.amount; + const cryptonote::tx_destination_entry& d = dsts[i]; + const bool dest_is_subtractable = subtract_fee_from_outputs.count(i); + const uint64_t fee_deduction = dest_is_subtractable ? subtractable_fee_deduction : 0; + const uint64_t required_amount = d.amount - std::min(fee_deduction, d.amount); + required[d.addr].first += required_amount; required[d.addr].second = d.is_subaddress; } diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 877e5afd0..3144a8fd3 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -139,6 +139,7 @@ private: public: // Full wallet callbacks virtual void on_new_block(uint64_t height, const cryptonote::block& block) {} + virtual void on_reorg(uint64_t height, uint64_t blocks_detached, size_t transfers_detached) {} virtual void on_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t amount, uint64_t burnt, const cryptonote::subaddress_index& subaddr_index, bool is_change, uint64_t unlock_time) {} virtual void on_unconfirmed_money_received(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t amount, const cryptonote::subaddress_index& subaddr_index) {} virtual void on_money_spent(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& in_tx, uint64_t amount, const cryptonote::transaction& spend_tx, const cryptonote::subaddress_index& subaddr_index) {} @@ -598,6 +599,7 @@ private: typedef std::vector<transfer_details> transfer_container; typedef serializable_unordered_multimap<crypto::hash, payment_details> payment_container; + typedef std::set<uint32_t> unique_index_container; struct multisig_sig { @@ -1111,11 +1113,11 @@ private: bool parse_unsigned_tx_from_str(const std::string &unsigned_tx_st, unsigned_tx_set &exported_txs) const; bool load_tx(const std::string &signed_filename, std::vector<tools::wallet2::pending_tx> &ptx, std::function<bool(const signed_tx_set&)> accept_func = NULL); bool parse_tx_from_str(const std::string &signed_tx_st, std::vector<tools::wallet2::pending_tx> &ptx, std::function<bool(const signed_tx_set &)> accept_func); - 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, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices); // pass subaddr_indices by value on purpose + 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, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const unique_index_container& subtract_fee_from_outputs = {}); // pass subaddr_indices by value on purpose std::vector<wallet2::pending_tx> create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices); std::vector<wallet2::pending_tx> create_transactions_single(const crypto::key_image &ki, const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra); std::vector<wallet2::pending_tx> create_transactions_from(const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, std::vector<size_t> unused_transfers_indices, std::vector<size_t> unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra); - bool sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, std::vector<cryptonote::tx_destination_entry> dsts) const; + bool sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, const std::vector<cryptonote::tx_destination_entry>& dsts, const unique_index_container& subtract_fee_from_outputs = {}) const; void cold_tx_aux_import(const std::vector<pending_tx>& ptx, const std::vector<std::string>& tx_device_aux); void cold_sign_tx(const std::vector<pending_tx>& ptx_vector, signed_tx_set &exported_txs, std::vector<cryptonote::address_parse_info> &dsts_info, std::vector<std::string> & tx_device_aux); uint64_t cold_key_image_sync(uint64_t &spent, uint64_t &unspent); diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index fcf2ddd93..1f7e1c75d 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -85,6 +85,7 @@ namespace tools // tx_too_big // zero_amount // zero_destination + // subtract_fee_from_bad_index // wallet_rpc_error * // daemon_busy // no_connection_to_daemon @@ -779,6 +780,15 @@ namespace tools } }; //---------------------------------------------------------------------------------------------------- + struct subtract_fee_from_bad_index : public transfer_error + { + explicit subtract_fee_from_bad_index(std::string&& loc, long bad_index) + : transfer_error(std::move(loc), + "subtractfeefrom: bad index: " + std::to_string(bad_index) + " (indexes are 0-based)") + { + } + }; + //---------------------------------------------------------------------------------------------------- struct wallet_rpc_error : public wallet_logic_error { const std::string& request() const { return m_request; } diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index cecd79368..4e42d64cd 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -988,9 +988,9 @@ namespace tools return amount; } //------------------------------------------------------------------------------------------------------------------------------ - template<typename Ts, typename Tu, typename Tk> + template<typename Ts, typename Tu, typename Tk, typename Ta> bool wallet_rpc_server::fill_response(std::vector<tools::wallet2::pending_tx> &ptx_vector, - bool get_tx_key, Ts& tx_key, Tu &amount, Tu &fee, Tu &weight, std::string &multisig_txset, std::string &unsigned_txset, bool do_not_relay, + bool get_tx_key, Ts& tx_key, Tu &amount, Ta &amounts_by_dest, Tu &fee, Tu &weight, std::string &multisig_txset, std::string &unsigned_txset, bool do_not_relay, Ts &tx_hash, bool get_tx_hex, Ts &tx_blob, bool get_tx_metadata, Ts &tx_metadata, Tk &spent_key_images, epee::json_rpc::error &er) { for (const auto & ptx : ptx_vector) @@ -1007,6 +1007,12 @@ namespace tools fill(fee, ptx.fee); fill(weight, cryptonote::get_transaction_weight(ptx.tx)); + // add amounts by destination + tools::wallet_rpc::amounts_list abd; + for (const auto& dst : ptx.dests) + abd.amounts.push_back(dst.amount); + fill(amounts_by_dest, abd); + // add spent key images tools::wallet_rpc::key_image_list key_image_list; bool all_are_txin_to_key = std::all_of(ptx.tx.vin.begin(), ptx.tx.vin.end(), [&](const cryptonote::txin_v& s_e) -> bool @@ -1087,7 +1093,7 @@ namespace tools { uint64_t mixin = m_wallet->adjust_mixin(req.ring_size ? req.ring_size - 1 : 0); uint32_t priority = m_wallet->adjust_priority(req.priority); - std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_transactions_2(dsts, mixin, req.unlock_time, priority, extra, req.account_index, req.subaddr_indices); + std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_transactions_2(dsts, mixin, req.unlock_time, priority, extra, req.account_index, req.subaddr_indices, req.subtract_fee_from_outputs); if (ptx_vector.empty()) { @@ -1104,7 +1110,7 @@ namespace tools return false; } - return fill_response(ptx_vector, req.get_tx_key, res.tx_key, res.amount, res.fee, res.weight, res.multisig_txset, res.unsigned_txset, req.do_not_relay, + return fill_response(ptx_vector, req.get_tx_key, res.tx_key, res.amount, res.amounts_by_dest, res.fee, res.weight, res.multisig_txset, res.unsigned_txset, req.do_not_relay, res.tx_hash, req.get_tx_hex, res.tx_blob, req.get_tx_metadata, res.tx_metadata, res.spent_key_images, er); } catch (const std::exception& e) @@ -1152,7 +1158,7 @@ namespace tools return false; } - return fill_response(ptx_vector, req.get_tx_keys, res.tx_key_list, res.amount_list, res.fee_list, res.weight_list, res.multisig_txset, res.unsigned_txset, req.do_not_relay, + return fill_response(ptx_vector, req.get_tx_keys, res.tx_key_list, res.amount_list, res.amounts_by_dest_list, res.fee_list, res.weight_list, res.multisig_txset, res.unsigned_txset, req.do_not_relay, res.tx_hash_list, req.get_tx_hex, res.tx_blob_list, req.get_tx_metadata, res.tx_metadata_list, res.spent_key_images_list, er); } catch (const std::exception& e) @@ -1541,7 +1547,7 @@ namespace tools { std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_unmixable_sweep_transactions(); - return fill_response(ptx_vector, req.get_tx_keys, res.tx_key_list, res.amount_list, res.fee_list, res.weight_list, res.multisig_txset, res.unsigned_txset, req.do_not_relay, + return fill_response(ptx_vector, req.get_tx_keys, res.tx_key_list, res.amount_list, res.amounts_by_dest_list, res.fee_list, res.weight_list, res.multisig_txset, res.unsigned_txset, req.do_not_relay, res.tx_hash_list, req.get_tx_hex, res.tx_blob_list, req.get_tx_metadata, res.tx_metadata_list, res.spent_key_images_list, er); } catch (const std::exception& e) @@ -1601,7 +1607,7 @@ namespace tools uint32_t priority = m_wallet->adjust_priority(req.priority); std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_transactions_all(req.below_amount, dsts[0].addr, dsts[0].is_subaddress, req.outputs, mixin, req.unlock_time, priority, extra, req.account_index, subaddr_indices); - return fill_response(ptx_vector, req.get_tx_keys, res.tx_key_list, res.amount_list, res.fee_list, res.weight_list, res.multisig_txset, res.unsigned_txset, req.do_not_relay, + return fill_response(ptx_vector, req.get_tx_keys, res.tx_key_list, res.amount_list, res.amounts_by_dest_list, res.fee_list, res.weight_list, res.multisig_txset, res.unsigned_txset, req.do_not_relay, res.tx_hash_list, req.get_tx_hex, res.tx_blob_list, req.get_tx_metadata, res.tx_metadata_list, res.spent_key_images_list, er); } catch (const std::exception& e) @@ -1678,7 +1684,7 @@ namespace tools return false; } - return fill_response(ptx_vector, req.get_tx_key, res.tx_key, res.amount, res.fee, res.weight, res.multisig_txset, res.unsigned_txset, req.do_not_relay, + return fill_response(ptx_vector, req.get_tx_key, res.tx_key, res.amount, res.amounts_by_dest, res.fee, res.weight, res.multisig_txset, res.unsigned_txset, req.do_not_relay, res.tx_hash, req.get_tx_hex, res.tx_blob, req.get_tx_metadata, res.tx_metadata, res.spent_key_images, er); } catch (const std::exception& e) diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index 3088fd9c2..3308d1751 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -263,9 +263,9 @@ namespace tools bool not_open(epee::json_rpc::error& er); void handle_rpc_exception(const std::exception_ptr& e, epee::json_rpc::error& er, int default_error_code); - template<typename Ts, typename Tu, typename Tk> + template<typename Ts, typename Tu, typename Tk, typename Ta> bool fill_response(std::vector<tools::wallet2::pending_tx> &ptx_vector, - bool get_tx_key, Ts& tx_key, Tu &amount, Tu &fee, Tu &weight, std::string &multisig_txset, std::string &unsigned_txset, bool do_not_relay, + bool get_tx_key, Ts& tx_key, Tu &amount, Ta &amounts_by_dest, Tu &fee, Tu &weight, std::string &multisig_txset, std::string &unsigned_txset, bool do_not_relay, Ts &tx_hash, bool get_tx_hex, Ts &tx_blob, bool get_tx_metadata, Ts &tx_metadata, Tk &spent_key_images, epee::json_rpc::error &er); bool validate_transfer(const std::list<wallet_rpc::transfer_destination>& destinations, const std::string& payment_id, std::vector<cryptonote::tx_destination_entry>& dsts, std::vector<uint8_t>& extra, bool at_least_one_destination, epee::json_rpc::error& er); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index d5c6bfd6b..b6098d95c 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -47,7 +47,7 @@ // advance which version they will stop working with // Don't go over 32767 for any of these #define WALLET_RPC_VERSION_MAJOR 1 -#define WALLET_RPC_VERSION_MINOR 26 +#define WALLET_RPC_VERSION_MINOR 27 #define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor)) #define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR) namespace tools @@ -530,11 +530,23 @@ namespace wallet_rpc END_KV_SERIALIZE_MAP() }; + struct amounts_list + { + std::list<uint64_t> amounts; + + bool operator==(const amounts_list& other) const { return amounts == other.amounts; } + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amounts) + END_KV_SERIALIZE_MAP() + }; + struct single_transfer_response { std::string tx_hash; std::string tx_key; uint64_t amount; + amounts_list amounts_by_dest; uint64_t fee; uint64_t weight; std::string tx_blob; @@ -547,6 +559,7 @@ namespace wallet_rpc KV_SERIALIZE(tx_hash) KV_SERIALIZE(tx_key) KV_SERIALIZE(amount) + KV_SERIALIZE_OPT(amounts_by_dest, decltype(amounts_by_dest)()) KV_SERIALIZE(fee) KV_SERIALIZE(weight) KV_SERIALIZE(tx_blob) @@ -564,6 +577,7 @@ namespace wallet_rpc std::list<transfer_destination> destinations; uint32_t account_index; std::set<uint32_t> subaddr_indices; + std::set<uint32_t> subtract_fee_from_outputs; uint32_t priority; uint64_t ring_size; uint64_t unlock_time; @@ -577,6 +591,7 @@ namespace wallet_rpc KV_SERIALIZE(destinations) KV_SERIALIZE(account_index) KV_SERIALIZE(subaddr_indices) + KV_SERIALIZE_OPT(subtract_fee_from_outputs, decltype(subtract_fee_from_outputs)()) KV_SERIALIZE(priority) KV_SERIALIZE_OPT(ring_size, (uint64_t)0) KV_SERIALIZE(unlock_time) @@ -598,6 +613,7 @@ namespace wallet_rpc std::list<std::string> tx_hash_list; std::list<std::string> tx_key_list; std::list<uint64_t> amount_list; + std::list<amounts_list> amounts_by_dest_list; std::list<uint64_t> fee_list; std::list<uint64_t> weight_list; std::list<std::string> tx_blob_list; @@ -610,6 +626,7 @@ namespace wallet_rpc KV_SERIALIZE(tx_hash_list) KV_SERIALIZE(tx_key_list) KV_SERIALIZE(amount_list) + KV_SERIALIZE_OPT(amounts_by_dest_list, decltype(amounts_by_dest_list)()) KV_SERIALIZE(fee_list) KV_SERIALIZE(weight_list) KV_SERIALIZE(tx_blob_list) |