What changed, and why it matters
This Monero wallet commit adds stronger safety checks when handling unsigned, partially signed, and fully signed transactions before they are signed or broadcast. It prevents several risky situations: reusing the same coin across multiple transactions, sending change to an address that does not belong to the wallet, mismatched destination address types, and a byte-order bug when verifying encrypted payment amounts. These are defensive hardening fixes in transaction validation code paths that handle sensitive user funds.
Treat this commit as a security-hardening patch and include it in the next release. Wallet operators and integrators should upgrade, especially those using multisig, cold-signing, or unsigned/signed transaction file workflows. No immediate public incident response is indicated, but downstream forks and wallet implementations should review equivalent validation gaps.
Security signals we found
Prevention of duplicate input pubkeys across transaction sets (avoids double-spend / same-input reuse within a set)
Change-address ownership validation for unsigned transactions (prevents change theft / misdirected change)
Transaction-set amount validation: outputs <= inputs and change <= change-address payments (prevents value inflation / inconsistent accounting)
Destination type consistency check across transaction sets (normal vs subaddress mismatch detection)
Endianness fix in amount encoding reproduction during output validation
Hardened `load_multisig_tx` to require known key images and full validation before persisting tx metadata
Set-level validation applied to unsigned tx parsing, sign_tx, multisig sign/load, cold signing, and signed tx parsing
Evidence from the diff
The patch hardens transaction-set validation in Monero’s wallet. New checks include: check_consistent_ins_outs to detect duplicate inputs and inconsistent subaddress designations across a set of transactions; sanity_check_tx_construction_data to ensure outputs do not exceed inputs and change does not exceed payments to the change address; sanity_check_unsigned_tx to verify change-address ownership; and a fix replacing a raw memcpy of dest.amount into amnt_data.amount.bytes with rct::d2h(dest.amount) to correct an endianness issue when reproducing encoded amounts. The new set-level checks are wired into unsigned-tx parsing/signing, multisig loading/signing, cold signing, and signed-tx parsing.
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
@@ -7263,11 +7263,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)
{
@@ -7276,17 +7275,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(),
@@ -7298,6 +7308,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,
@@ -7827,12 +7856,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;
@@ -7854,6 +7890,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())
@@ -7988,8 +8026,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;
}
@@ -8128,23 +8166,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)
{
@@ -8236,8 +8271,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;
@@ -8301,13 +8335,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)
{
@@ -8343,6 +8374,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;
}
@@ -8378,6 +8427,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;
@@ -8562,14 +8615,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);
}
//----------------------------------------------------------------------------------------------------
@@ -11488,6 +11535,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");
@@ -11521,8 +11572,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
@@ -937,6 +937,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 71/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.