What changed, and why it matters
This Monero wallet patch adds stronger safety checks when a wallet prepares, signs, or loads multi-step transactions (unsigned transactions, multisig transactions, and cold-device transactions). It verifies that money going into the transaction is not counted twice, that change addresses really belong to the wallet, that outputs do not exceed inputs, and that destination address types stay consistent. These are defensive hardening measures against mistakes or maliciously crafted transaction files that could otherwise make the wallet sign an invalid or harmful transaction.
Treat this as a security-hardening fix and include it in the next release. Users who create, sign, or load unsigned/multisig/cold transactions should upgrade. Wallet integrators should verify their transaction-export/import formats pass the new validation and review any custom tooling that constructs tx_construction_data or pending_tx structures.
Security signals we found
Adds duplicate-input detection across transaction sets
Adds destination address type consistency checks
Adds uint64 overflow guard for summed input amounts
Validates change address ownership against wallet account/subaddresses
Enforces output amount <= input amount and change <= change-address amount
Replaces memcpy amount encoding with proper rct::d2h conversion
Wires validation into unsigned tx parsing, signing, multisig, and cold-signing flows
Introduces set-level sanity checks (sanity_check_unsigned_tx_set, sanity_check_pending_tx_set)
Evidence from the diff
The commit hardens pending/unsigned/multisig transaction validation in the Monero wallet. New helpers check consistency across a set of transactions: no duplicate input public keys, consistent normal-vs-subaddress destination designations, and summed input amounts fitting in uint64_t. It also sanity-checks per-transaction construction data (outputs <= inputs, change amount <= change-destined amount) and validates that change addresses belong to the sender account. These checks are wired into parse_unsigned_tx_from_str, sign_tx, make_multisig_tx_set, parse_multisig_tx_from_str, load_multisig_tx, sign_multisig_tx, cold_sign_tx, and signed-tx parsing. A memcpy-based amount encoding bug is also fixed by using rct::d2h. The patch is defensive and closes classes of transaction-manipulation issues, but the commit message frames it as hardening rather than a specific disclosed vulnerability.
Changed components
src/wallet/pending_tx_validation.cppsrc/wallet/pending_tx_validation.hsrc/wallet/wallet2.cppsrc/wallet/wallet2.hInspect captured patch +265 / −45
### src/wallet/pending_tx_validation.cpp
@@ -55,6 +55,7 @@
#include <algorithm>
#include <cstdint>
#include <string>
+#include <unordered_map>
#include <vector>
#undef MONERO_DEFAULT_LOG_CATEGORY
@@ -65,6 +66,87 @@ namespace tools
namespace wallet
{
//-------------------------------------------------------------------------------------------------------------------
+static const std::vector<cryptonote::tx_destination_entry>& get_tx_destinations(const wallet2::tx_construction_data &tx)
+{
+ return tx.splitted_dsts;
+}
+//-------------------------------------------------------------------------------------------------------------------
+static const std::vector<cryptonote::tx_destination_entry>& get_tx_destinations(const wallet2::pending_tx &tx)
+{
+ return tx.construction_data.splitted_dsts;
+}
+//-------------------------------------------------------------------------------------------------------------------
+static const std::vector<cryptonote::tx_source_entry>& get_tx_sources(const wallet2::tx_construction_data &tx)
+{
+ return tx.sources;
+}
+//-------------------------------------------------------------------------------------------------------------------
+static const std::vector<cryptonote::tx_source_entry>& get_tx_sources(const wallet2::pending_tx &tx)
+{
+ return tx.construction_data.sources;
+}
+//-------------------------------------------------------------------------------------------------------------------
+template<typename T>
+static bool check_consistent_ins_outs_impl(const std::vector<T> &txes)
+{
+ std::unordered_set<rct::key> seen_ins;
+ std::unordered_map<cryptonote::account_public_address, bool> destination_types{};
+ boost::multiprecision::uint128_t total_input_amount = 0;
+ for (const auto &tx: txes)
+ {
+ // Inputs
+ for (const auto &src: get_tx_sources(tx))
+ {
+ CHECK_AND_ASSERT_THROW_MES(src.real_output < src.outputs.size(),
+ "check_consistent_ins_outs: ring sig index " << src.real_output << " out of input set size "
+ << src.outputs.size());
+ const auto &dest = src.outputs[src.real_output].second.dest;
+ const auto result = seen_ins.emplace(dest);
+ CHECK_AND_ASSERT_THROW_MES(result.second,
+ "check_consistent_ins_outs: duplicate input pubkey");
+ total_input_amount += src.amount;
+ }
+
+ // Outputs
+ // Keep the address type consistent across transactions, including zero-amount
+ // outputs and change.
+ // We only care about normal vs subaddress consistency, not integrated address consistency.
+ for (const auto &dest: get_tx_destinations(tx))
+ {
+ const auto result = destination_types.emplace(dest.addr, dest.is_subaddress);
+ CHECK_AND_ASSERT_THROW_MES(result.second || result.first->second == dest.is_subaddress,
+ "check_consistent_ins_outs: duplicate destinations do not have matching subaddress designations");
+ }
+ }
+ CHECK_AND_ASSERT_THROW_MES(total_input_amount <= UINT64_MAX,
+ "check_consistent_ins_outs: tx set input amount > 2^64 - 1");
+ return true;
+}
+//-------------------------------------------------------------------------------------------------------------------
+static void sanity_check_unsigned_tx(const wallet2::tx_construction_data &tx,
+ const cryptonote::account_keys &account_keys,
+ const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses)
+{
+ sanity_check_tx_construction_data(tx);
+
+ const auto &change = tx.change_dts;
+ const bool pays_change = std::any_of(tx.splitted_dsts.begin(), tx.splitted_dsts.end(),
+ [&change](const cryptonote::tx_destination_entry &dest) {
+ return dest.amount > 0 && dest.addr == change.addr;
+ });
+
+ // Zero-valued dummy change outputs deliberately use an unrelated address.
+ if (change.amount == 0 && !pays_change)
+ return;
+
+ if (cryptonote::sanity_check_change_address(change.addr, subaddresses, account_keys))
+ return;
+
+ const auto expected_change = account_keys.get_device().get_subaddress(account_keys, {tx.subaddr_account, 0});
+ CHECK_AND_ASSERT_THROW_MES(change.addr == expected_change,
+ "sanity_check_unsigned_tx: change address does not belong to the sender account");
+}
+//-------------------------------------------------------------------------------------------------------------------
static void validate_tx_outs(
const wallet2::pending_tx &ptx,
const std::vector<cryptonote::txout_to_tagged_key> &ext_outputs,
@@ -160,7 +242,7 @@ static void validate_tx_outs(
// - encoded amount
rct::ecdhTuple amnt_data{};
- memcpy(amnt_data.amount.bytes, &dest.amount, sizeof(dest.amount));
+ amnt_data.amount = rct::d2h(dest.amount);
rct::ecdhDecode(amnt_data, amount_keys.at(i), true); //v2, decode gives mask
CHECK_AND_ASSERT_THROW_MES(ptx.tx.rct_signatures.ecdhInfo.at(i).amount == amnt_data.amount,
"validate_tx_outs: failed reproducing encoded amount");
@@ -371,11 +453,13 @@ void sanity_check_pending_tx(const wallet2::pending_tx &ptx,
// - For SOME REASON, construction data sources are not always in the same order as inputs, so we need to fix that
auto sources_ordered = construct.sources;
std::vector<size_t> ins_order;
+ std::unordered_set<crypto::public_key> seen_ins;
for (const size_t selected_transfer : ptx.selected_transfers)
{
CHECK_AND_ASSERT_THROW_MES(selected_transfer < transfers.size(),
"sanity_check_pending_tx: invalid transfers index");
const auto &transfer = transfers.at(selected_transfer);
+ const auto transfer_pkey = transfer.get_public_key();
for (size_t i = 0; i < sources_ordered.size(); ++i)
{
const auto &src = sources_ordered.at(i);
@@ -386,11 +470,16 @@ void sanity_check_pending_tx(const wallet2::pending_tx &ptx,
// 'index in global array of same-amount outputs'.
if (src.outputs[src.real_output].first != transfer.m_global_output_index
|| src.amount != transfer.m_amount
- || src.outputs[src.real_output].second.dest != rct::pk2rct(transfer.get_public_key()))
+ || src.outputs[src.real_output].second.dest != rct::pk2rct(transfer_pkey))
continue;
ins_order.push_back(i);
break;
}
+ // We check for duplicate onetime addr instead of selected_transfer in case of transfers with
+ // the same destination. Note that we assume `transfers` is sanitized of non-canonical pubkey representations.
+ CHECK_AND_ASSERT_THROW_MES(seen_ins.count(transfer_pkey) == 0,
+ "sanity_check_pending_tx: duplicate input pubkey");
+ seen_ins.insert(transfer_pkey);
}
CHECK_AND_ASSERT_THROW_MES(ins_order.size() == sources_ordered.size(),
"sanity_check_pending_tx: global index mismatch between sources and tx");
@@ -766,5 +855,62 @@ void sanity_check_pending_tx(const wallet2::pending_tx &ptx,
"sanity_check_pending_tx: output amount != input amount");
}
//-------------------------------------------------------------------------------------------------------------------
+void check_consistent_ins_outs(const std::vector<wallet2::tx_construction_data> &txes)
+{
+ check_consistent_ins_outs_impl(txes);
+}
+//-------------------------------------------------------------------------------------------------------------------
+void check_consistent_ins_outs(const std::vector<wallet2::pending_tx> &txes)
+{
+ check_consistent_ins_outs_impl(txes);
+}
+//-------------------------------------------------------------------------------------------------------------------
+void sanity_check_tx_construction_data(const wallet2::tx_construction_data &tx)
+{
+ boost::multiprecision::uint128_t input_amount = 0;
+ boost::multiprecision::uint128_t output_amount = 0;
+ boost::multiprecision::uint128_t amount_to_change_address = 0;
+ for (const auto &src: tx.sources)
+ input_amount += src.amount;
+ for (const auto &dest: tx.splitted_dsts)
+ {
+ output_amount += dest.amount;
+ if (dest.addr == tx.change_dts.addr)
+ amount_to_change_address += dest.amount;
+ }
+ CHECK_AND_ASSERT_THROW_MES(output_amount <= input_amount,
+ "sanity_check_tx_construction_data: output amount exceeds input amount");
+ CHECK_AND_ASSERT_THROW_MES(tx.change_dts.amount <= amount_to_change_address,
+ "sanity_check_tx_construction_data: change exceeds payment to change address");
+}
+//-------------------------------------------------------------------------------------------------------------------
+void sanity_check_unsigned_tx_set(const std::vector<wallet2::tx_construction_data> &txes,
+ const cryptonote::account_keys &account_keys,
+ const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses)
+{
+ check_consistent_ins_outs(txes);
+
+ for (const auto &tx: txes)
+ sanity_check_unsigned_tx(tx, account_keys, subaddresses);
+}
+//-------------------------------------------------------------------------------------------------------------------
+void sanity_check_pending_tx_set(const std::vector<wallet2::pending_tx> &ptxs,
+ const cryptonote::network_type nettype,
+ const cryptonote::account_keys &account_keys,
+ const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses,
+ const std::vector<wallet2_basic::transfer_details> &transfers,
+ const bool redacted,
+ const std::optional<std::function<const crypto::key_image(const size_t)>> &transfer_ki_resolver,
+ const bool allow_read_only)
+{
+ check_consistent_ins_outs(ptxs);
+
+ for (const auto &ptx: ptxs)
+ {
+ sanity_check_pending_tx(ptx, nettype, account_keys, subaddresses, transfers,
+ redacted, transfer_ki_resolver, allow_read_only);
+ }
+}
+//-------------------------------------------------------------------------------------------------------------------
} //namespace wallet
} //namespace tools
### src/wallet/pending_tx_validation.h
@@ -48,6 +48,20 @@ namespace tools
{
namespace wallet
{
+// Checks that inputs are not duplicated across transactions in the set.
+// Checks that duplicate destination addresses across the tx set have the same normal/subaddress
+// designations.
+// Checks that input amounts summed across the tx set fit in uint64_t.
+// *Only* for checking consistency between txs (which may be proposals, partially signed, or fully signed).
+// Use `sanity_check_pending_tx` to validate internal consistency for individual `pending_tx`, or
+// `sanity_check_pending_tx_set` to check consistent ins/outs + internals.
+void check_consistent_ins_outs(const std::vector<wallet2::tx_construction_data> &txes);
+void check_consistent_ins_outs(const std::vector<wallet2::pending_tx> &txes);
+void sanity_check_tx_construction_data(const wallet2::tx_construction_data &tx);
+// Checks ins/outs consistency across the set, then validates each unsigned transaction.
+void sanity_check_unsigned_tx_set(const std::vector<wallet2::tx_construction_data> &txes,
+ const cryptonote::account_keys &account_keys,
+ const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses);
/**
* brief: sanity_check_pending_tx - validate `pending_tx` consistency with itself and with with `transfer_details`
* Assumes `ptx` version is >= v16.
@@ -77,5 +91,14 @@ void sanity_check_pending_tx(const wallet2::pending_tx &ptx,
const bool redacted,
const std::optional<std::function<const crypto::key_image(const size_t)>> &transfer_ki_resolver,
const bool allow_read_only);
+// Checks ins/outs consistency across the set, then validates each pending transaction.
+void sanity_check_pending_tx_set(const std::vector<wallet2::pending_tx> &ptxs,
+ const cryptonote::network_type nettype,
+ const cryptonote::account_keys &account_keys,
+ const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses,
+ const std::vector<wallet2_basic::transfer_details> &transfers,
+ const bool redacted,
+ const std::optional<std::function<const crypto::key_image(const size_t)>> &transfer_ki_resolver,
+ const bool allow_read_only);
} //namespace wallet
} //namespace tools
### src/wallet/wallet2.cpp
@@ -7403,11 +7403,10 @@ void wallet2::get_unconfirmed_payments(std::list<std::pair<crypto::hash,wallet2:
//----------------------------------------------------------------------------------------------------
// `expect_imported_key_images = false` overlaps with `transfer_ki_resolver = std::nullopt` but
// we include both to reduce callsite complexity. (lack of useful enums in C++)
-void wallet2::sanity_check_pending_tx(const wallet2::pending_tx &ptx,
- const bool redacted,
+static std::optional<std::function<const crypto::key_image(const size_t)>> make_transfer_ki_resolver(
+ const wallet2::transfer_container &transfers,
const bool expect_imported_key_images,
- std::optional<std::function<const crypto::key_image(const size_t)>> transfer_ki_resolver,
- const bool allow_read_only) const
+ std::optional<std::function<const crypto::key_image(const size_t)>> transfer_ki_resolver)
{
if (expect_imported_key_images)
{
@@ -7416,17 +7415,28 @@ void wallet2::sanity_check_pending_tx(const wallet2::pending_tx &ptx,
"sanity_check_pending_tx (wallet2): expected imported key images but a ki resolver was provided");
const std::function<const crypto::key_image(const size_t)> temp =
- [this](const size_t i)
+ [&transfers](const size_t i)
{
- CHECK_AND_ASSERT_THROW_MES(i < m_transfers.size(),
+ CHECK_AND_ASSERT_THROW_MES(i < transfers.size(),
"sanity_check_pending_tx (wallet2): transfer - selected transfer idx out of known transfers");
- const auto &transfer = m_transfers.at(i);
+ const auto &transfer = transfers.at(i);
CHECK_AND_ASSERT_THROW_MES(transfer.m_key_image_known,
"sanity_check_pending_tx (wallet2): transfer - KI is expected but unknown");
return transfer.m_key_image;
};
transfer_ki_resolver = temp;
}
+ return transfer_ki_resolver;
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::sanity_check_pending_tx(const wallet2::pending_tx &ptx,
+ const bool redacted,
+ const bool expect_imported_key_images,
+ std::optional<std::function<const crypto::key_image(const size_t)>> transfer_ki_resolver,
+ const bool allow_read_only) const
+{
+ transfer_ki_resolver = make_transfer_ki_resolver(m_transfers,
+ expect_imported_key_images, std::move(transfer_ki_resolver));
wallet::sanity_check_pending_tx(ptx,
this->nettype(),
@@ -7438,6 +7448,25 @@ void wallet2::sanity_check_pending_tx(const wallet2::pending_tx &ptx,
allow_read_only);
}
//----------------------------------------------------------------------------------------------------
+void wallet2::sanity_check_pending_tx_set(const std::vector<pending_tx> &ptxs,
+ const bool redacted,
+ const bool expect_imported_key_images,
+ std::optional<std::function<const crypto::key_image(const size_t)>> transfer_ki_resolver,
+ const bool allow_read_only) const
+{
+ transfer_ki_resolver = make_transfer_ki_resolver(m_transfers,
+ expect_imported_key_images, std::move(transfer_ki_resolver));
+
+ wallet::sanity_check_pending_tx_set(ptxs,
+ this->nettype(),
+ m_account.get_keys(),
+ m_subaddresses,
+ m_transfers,
+ redacted,
+ transfer_ki_resolver,
+ allow_read_only);
+}
+//----------------------------------------------------------------------------------------------------
void wallet2::rescan_spent()
{
// This is RPC call that can take a long time if there are many outputs,
@@ -7967,12 +7996,19 @@ bool wallet2::parse_unsigned_tx_from_str(const std::string &unsigned_tx_st, unsi
LOG_PRINT_L0("Failed to parse data from unsigned tx");
return false;
}
+ try { wallet::sanity_check_unsigned_tx_set(exported_txs.txes, m_account.get_keys(), m_subaddresses); }
+ catch (const std::exception &e)
+ {
+ LOG_PRINT_L0("Failed to validate unsigned txs: " << e.what());
+ return false;
+ }
}
else
{
LOG_PRINT_L0("Unsupported version in unsigned tx");
return false;
}
+
LOG_PRINT_L1("Loaded tx unsigned data from binary: " << exported_txs.txes.size() << " transactions");
return true;
@@ -7994,6 +8030,8 @@ bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &s
//----------------------------------------------------------------------------------------------------
bool wallet2::sign_tx(unsigned_tx_set &exported_txs, std::vector<wallet2::pending_tx> &txs, signed_tx_set &signed_txes)
{
+ wallet::sanity_check_unsigned_tx_set(exported_txs.txes, m_account.get_keys(), m_subaddresses);
+
if (!std::get<2>(exported_txs.new_transfers).empty())
import_outputs(exported_txs.new_transfers);
else if (!std::get<2>(exported_txs.transfers).empty())
@@ -8128,8 +8166,8 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, std::vector<wallet2::pendin
"Cold wallet signing: No record of output " + std::to_string(idx) + " in this wallet, run "
"`export_outputs all` on the online wallet and `import_outputs` here.");
}
- this->sanity_check_pending_tx(ptx, false, true, std::nullopt, false);
}
+ this->sanity_check_pending_tx_set(txs, false, true, std::nullopt, false);
return true;
}
@@ -8268,23 +8306,20 @@ bool wallet2::parse_tx_from_str(const std::string &signed_tx_st, std::vector<too
{
// validate before mutating state or displaying to user
// - Verify with the assumption signed txs are redacted.
- for (const auto &ptx : signed_txs.ptx)
- {
- // Manually check `signed_txs.key_images` so local state is not mutated before we validate.
- // We inject the key image checker because `ptx` internal sorting ambiguity makes it cumbersome to
- // directly validate key images here.
- // NOTE: These txs may be READ-ONLY, which means spent/frozen inputs are allowed.
- const auto &kis = signed_txs.key_images;
- this->sanity_check_pending_tx(ptx,
- true,
- false,
- { [&kis](const size_t i) {
- CHECK_AND_ASSERT_THROW_MES(i < kis.size(), "failed loading signed tx: ptx selected transfer "
- "is outside the bounds of imported key images");
- return kis.at(i);
- } },
- true);
- }
+ // Manually check `signed_txs.key_images` so local state is not mutated before we validate.
+ // We inject the key image checker because `ptx` internal sorting ambiguity makes it cumbersome to
+ // directly validate key images here.
+ // NOTE: These txs may be READ-ONLY, which means spent/frozen inputs are allowed.
+ const auto &kis = signed_txs.key_images;
+ this->sanity_check_pending_tx_set(signed_txs.ptx,
+ true,
+ false,
+ { [&kis](const size_t i) {
+ CHECK_AND_ASSERT_THROW_MES(i < kis.size(), "failed loading signed tx: ptx selected transfer "
+ "is outside the bounds of imported key images");
+ return kis.at(i);
+ } },
+ true);
}
catch (const std::exception &e)
{
@@ -8376,8 +8411,7 @@ bool wallet2::save_multisig_tx(const multisig_tx_set &txs, const std::string &fi
//----------------------------------------------------------------------------------------------------
wallet2::multisig_tx_set wallet2::make_multisig_tx_set(const std::vector<pending_tx>& ptx_vector) const
{
- for (const auto &ptx : ptx_vector)
- this->sanity_check_pending_tx(ptx, false, true, std::nullopt, false);
+ this->sanity_check_pending_tx_set(ptx_vector, false, true, std::nullopt, false);
multisig_tx_set txs;
txs.m_ptx = ptx_vector;
@@ -8441,13 +8475,10 @@ bool wallet2::parse_multisig_tx_from_str(std::string multisig_tx_st, multisig_tx
try
{
// sanity checks
- for (const auto &ptx: exported_txs.m_ptx)
- {
- // If key images have not been imported then this could fail if we check key images. We'd rather fail elsewhere
- // with a better error message.
- // Note: These may be READ-ONLY, so spent/frozen inputs are allowed.
- this->sanity_check_pending_tx(ptx, false, false, std::nullopt, true);
- }
+ // If key images have not been imported then this could fail if we check key images. We'd rather fail elsewhere
+ // with a better error message.
+ // Note: These may be READ-ONLY, so spent/frozen inputs are allowed.
+ this->sanity_check_pending_tx_set(exported_txs.m_ptx, false, false, std::nullopt, true);
}
catch (const std::exception &e)
{
@@ -8483,6 +8514,24 @@ bool wallet2::load_multisig_tx(cryptonote::blobdata s, multisig_tx_set &exported
const crypto::hash txid = get_transaction_hash(ptx.tx);
if (store_tx_info())
{
+ // SPECIAL CONDITION: before saving any info about the tx we should fully validate it. This
+ // requires knowing the key images. parse_multisig_tx_from_str() is allowed to treat the
+ // tx as informational, which permits unknown key images, but here we enforce it.
+ try
+ {
+ for (const auto idx : ptx.construction_data.selected_transfers)
+ {
+ if (idx >= m_transfers.size() || !m_transfers[idx].m_key_image_known)
+ THROW_WALLET_EXCEPTION(error::multisig_import_needed);
+ }
+ this->sanity_check_pending_tx(ptx, false, true, std::nullopt, true);
+ }
+ catch (const std::exception &e)
+ {
+ LOG_PRINT_L0("load_multisig_tx failed: " << e.what());
+ return false;
+ }
+
m_tx_keys[txid] = ptx.tx_key;
m_additional_tx_keys[txid] = ptx.additional_tx_keys;
}
@@ -8518,6 +8567,10 @@ bool wallet2::load_multisig_tx_from_file(const std::string &filename, multisig_t
//----------------------------------------------------------------------------------------------------
bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs_inout, std::vector<crypto::hash> &txids)
{
+ wallet::check_consistent_ins_outs(exported_txs_inout.m_ptx);
+ for (const auto &ptx: exported_txs_inout.m_ptx)
+ wallet::sanity_check_tx_construction_data(ptx.construction_data);
+
multisig_tx_set exported_txs = exported_txs_inout;
std::vector<crypto::hash> signed_txids;
std::vector<std::pair<crypto::hash, size_t>> signed_tx_key_indices;
@@ -8702,14 +8755,8 @@ bool wallet2::sign_multisig_tx_to_file(multisig_tx_set &exported_txs, const std:
bool wallet2::sign_multisig_tx_from_file(const std::string &filename, std::vector<crypto::hash> &txids, std::function<bool(const multisig_tx_set&)> accept_func)
{
multisig_tx_set exported_txs;
- if(!load_multisig_tx_from_file(filename, exported_txs))
+ if(!load_multisig_tx_from_file(filename, exported_txs, accept_func))
return false;
-
- if (accept_func && !accept_func(exported_txs))
- {
- LOG_PRINT_L1("Transactions rejected by callback");
- return false;
- }
return sign_multisig_tx_to_file(exported_txs, filename, txids);
}
//----------------------------------------------------------------------------------------------------
@@ -11628,6 +11675,10 @@ void wallet2::cold_tx_aux_import(const std::vector<pending_tx> & ptx, const std:
//----------------------------------------------------------------------------------------------------
void wallet2::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)
{
+ wallet::check_consistent_ins_outs(ptx_vector);
+ for (const auto &ptx: ptx_vector)
+ wallet::sanity_check_tx_construction_data(ptx.construction_data);
+
auto & hwdev = get_account().get_device();
if (!hwdev.has_tx_cold_sign()){
throw std::invalid_argument("Device does not support cold sign protocol");
@@ -11661,8 +11712,7 @@ void wallet2::cold_sign_tx(const std::vector<pending_tx>& ptx_vector, signed_tx_
// For robustness it would be better for both devices to distrust each other. Note that all redacted
// info is left in plaintext in the `pending_tx` construction data, so it's unclear *why* anything
// is redacted in the first place.
- for (const auto &ptx : exported_txs.ptx)
- this->sanity_check_pending_tx(ptx, true, false, std::nullopt, false);
+ this->sanity_check_pending_tx_set(exported_txs.ptx, true, false, std::nullopt, false);
// Print
for (auto &c_ptx: exported_txs.ptx) LOG_PRINT_L0(cryptonote::obj_to_json_str(c_ptx.tx));
### src/wallet/wallet2.h
@@ -969,6 +969,7 @@ namespace tools
void get_unconfirmed_payments_out(std::list<std::pair<crypto::hash,wallet2::unconfirmed_transfer_details>>& unconfirmed_payments, const boost::optional<uint32_t>& subaddr_account = boost::none, const std::set<uint32_t>& subaddr_indices = {}) const;
void get_unconfirmed_payments(std::list<std::pair<crypto::hash,wallet2::pool_payment_details>>& unconfirmed_payments, const boost::optional<uint32_t>& subaddr_account = boost::none, const std::set<uint32_t>& subaddr_indices = {}) const;
void sanity_check_pending_tx(const wallet2::pending_tx &ptx, const bool redacted, const bool expect_imported_key_images, std::optional<std::function<const crypto::key_image(const size_t)>> transfer_ki_resolver, const bool allow_read_only) const;
+ void sanity_check_pending_tx_set(const std::vector<pending_tx> &ptxs, const bool redacted, const bool expect_imported_key_images, std::optional<std::function<const crypto::key_image(const size_t)>> transfer_ki_resolver, const bool allow_read_only) const;
uint64_t get_blockchain_current_height() const { return m_blockchain.size(); }
void rescan_spent();Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.