Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions src/qt/mnsharesession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,6 @@ bool SameShare(const MnShareSession::Share& a, const MnShareSession::Share& b)
a.rewardAddress == b.rewardAddress;
}

bool SameContribution(const MnShareSession::Contribution& a, const MnShareSession::Contribution& b)
{
if (a.inputs.size() != b.inputs.size() || a.hasChange != b.hasChange) return false;
if (a.hasChange && (a.changeAddress != b.changeAddress || a.changeAmount != b.changeAmount)) return false;
for (size_t i = 0; i < a.inputs.size(); ++i) {
if (a.inputs[i].txid.compare(b.inputs[i].txid, Qt::CaseInsensitive) != 0 ||
a.inputs[i].vout != b.inputs[i].vout || a.inputs[i].sequence != b.inputs[i].sequence) {
return false;
}
}
return true;
}

bool IsTxidHex(const QString& txid)
{
return txid.size() == 64 && IsHex(txid.toStdString());
Expand Down Expand Up @@ -191,6 +178,19 @@ bool DecodeSharedProTx(const QString& protx_hex, CMutableTransaction& tx, CProRe

} // anonymous namespace

bool MnShareSession::SameContribution(const Contribution& a, const Contribution& b)
{
if (a.inputs.size() != b.inputs.size() || a.hasChange != b.hasChange) return false;
if (a.hasChange && (a.changeAddress != b.changeAddress || a.changeAmount != b.changeAmount)) return false;
for (size_t i = 0; i < a.inputs.size(); ++i) {
if (a.inputs[i].txid.compare(b.inputs[i].txid, Qt::CaseInsensitive) != 0 ||
a.inputs[i].vout != b.inputs[i].vout || a.inputs[i].sequence != b.inputs[i].sequence) {
return false;
}
}
return true;
}

namespace {
//! U+202F NARROW NO-BREAK SPACE. BitcoinUnits groups thousands with a plain
//! thin space, which a layout may break a number across.
Expand Down
3 changes: 3 additions & 0 deletions src/qt/mnsharesession.h
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ class MnShareSession

//! Whether a row with `share`'s owner address has the same amount and addresses here
bool hasShare(const Share& share) const;
//! Same inputs, in order and with their sequences, and the same change.
//! Labels and changeIndex are ignored.
static bool SameContribution(const Contribution& a, const Contribution& b);
//! Whether a contribution with `contribution`'s label spends the same coins and pays the same change here
bool hasContribution(const Contribution& contribution) const;

Expand Down
126 changes: 95 additions & 31 deletions src/qt/sharedmncreatedialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1986,8 +1986,18 @@ void SharedMnCreateDialog::refreshContributionPage()
const CAmount target{contributionTarget()};
CoinSelection selection;
QString error;
CAmount again_total{0};
if (target <= 0) {
m_coins_label->setText(tr("Choose which participant you are."));
} else if (const auto* again{reservableAgain(&again_total)}) {
const QString coins{CoinCount(again->inputs.size())};
m_coins_label->setText(
again->hasChange
? tr("Uses %1 (%2) as reserved before; change %3 returns to the same address of yours.")
.arg(coins, FormatAmount(m_wallet_model, again_total),
FormatAmount(m_wallet_model, again->changeAmount))
: tr("Uses %1 (%2) as reserved before, with no change left over.")
.arg(coins, FormatAmount(m_wallet_model, again_total)));
} else if (selectCoins(target, selection, error)) {
const QString coins{CoinCount(selection.inputs.size())};
m_coins_label->setText(
Expand Down Expand Up @@ -2358,6 +2368,7 @@ void SharedMnCreateDialog::startSession()
unlockAllContributedCoins();
m_coins_released = false;
m_released_contributions.clear();
m_built_contributions.clear();
m_session = MnShareSession();
m_role = Role::Coordinator;
m_my_share = 0;
Expand Down Expand Up @@ -3372,23 +3383,19 @@ CAmount SharedMnCreateDialog::contributionTarget() const
return m_role == Role::Coordinator ? share + m_fee_field->value() : share;
}

bool SharedMnCreateDialog::selectCoins(CAmount target, CoinSelection& selection, QString& error) const
bool SharedMnCreateDialog::isDust(CAmount value) const
{
selection = CoinSelection{};
error.clear();
if (m_wallet_model == nullptr) {
error = tr("No wallet is available.");
return false;
}
if (target <= 0) {
error = tr("Select the participant that is you.");
return false;
}
const CTxOut probe{value, GetScriptForDestination(PKHash{CKeyID{}})};
return IsDust(probe, m_node.getDustRelayFee());
}

std::vector<std::pair<CAmount, COutPoint>> SharedMnCreateDialog::fundingCandidates() const
{
std::vector<std::pair<CAmount, COutPoint>> candidates;
if (m_wallet_model == nullptr) return candidates;
// listCoins() already holds back unconfirmed coins the wallet does not
// trust, so an InstantSend-locked coin or this wallet's own unconfirmed
// change can fund a share, as it can any other payment
std::vector<std::pair<CAmount, COutPoint>> candidates;
for (const auto& [dest, coins] : m_wallet_model->wallet().listCoins()) {
for (const auto& [outpoint, txout] : coins) {
if (txout.is_spent) continue;
Expand All @@ -3397,6 +3404,58 @@ bool SharedMnCreateDialog::selectCoins(CAmount target, CoinSelection& selection,
candidates.emplace_back(txout.txout.nValue, outpoint);
}
}
return candidates;
}

bool SharedMnCreateDialog::fundsShareAgain(const MnShareSession::Contribution& released, CAmount* total_out) const
{
if (m_wallet_model == nullptr || released.inputs.empty()) return false;
const auto candidates{fundingCandidates()};
CAmount total{0};
for (const auto& input : released.inputs) {
const COutPoint outpoint{uint256S(input.txid.toStdString()), input.vout};
const auto coin{std::find_if(candidates.begin(), candidates.end(),
[&](const auto& candidate) { return candidate.second == outpoint; })};
if (coin == candidates.end()) return false;
Comment thread
PastaPastaPasta marked this conversation as resolved.
Resolved
total += coin->first;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
if (total_out != nullptr) *total_out = total;
const CAmount target{contributionTarget()};
if (released.hasChange) return total - released.changeAmount == target;
// Without change, a dust remainder goes to the fee, as selectCoins() leaves it
return total == target || (total > target && isDust(total - target));
}

const MnShareSession::Contribution* SharedMnCreateDialog::reservableAgain(CAmount* total_out) const
{
if (m_my_share < 0 || static_cast<size_t>(m_my_share) >= m_session.shares().size()) return nullptr;
const QString& label{m_session.shares()[m_my_share].label};
// Only a contribution this wallet built itself goes back: one absorbed from
// a reply under this share's name can carry anything the sender chose
for (auto released{m_released_contributions.rbegin()}; released != m_released_contributions.rend(); ++released) {
if (released->label != label) continue;
const auto built{std::find_if(m_built_contributions.begin(), m_built_contributions.end(), [&](const auto& c) {
return c.label == label && MnShareSession::SameContribution(c, *released);
})};
if (built != m_built_contributions.end() && fundsShareAgain(*built, total_out)) return &*built;
}
return nullptr;
}

bool SharedMnCreateDialog::selectCoins(CAmount target, CoinSelection& selection, QString& error) const
{
selection = CoinSelection{};
error.clear();
if (m_wallet_model == nullptr) {
error = tr("No wallet is available.");
return false;
}
if (target <= 0) {
error = tr("Select the participant that is you.");
return false;
}

std::vector<std::pair<CAmount, COutPoint>> candidates{fundingCandidates()};
// Largest first keeps the input count, and with it everyone's signing work
// and the transaction fee, as small as possible.
std::sort(candidates.begin(), candidates.end(),
Expand All @@ -3405,13 +3464,9 @@ bool SharedMnCreateDialog::selectCoins(CAmount target, CoinSelection& selection,
// A change output below the dust threshold cannot be relayed, so keep
// adding coins until the change clears it; if nothing does, drop the
// change and let the remainder go to the fee.
const auto is_dust = [this](CAmount value) {
const CTxOut probe{value, GetScriptForDestination(PKHash{CKeyID{}})};
return IsDust(probe, m_node.getDustRelayFee());
};
size_t used{0};
for (; used < candidates.size(); ++used) {
if (selection.total >= target && (selection.total == target || !is_dust(selection.total - target))) break;
if (selection.total >= target && (selection.total == target || !isDust(selection.total - target))) break;
selection.total += candidates[used].first;
MnShareSession::Input input;
input.txid = QString::fromStdString(candidates[used].second.hash.ToString());
Expand All @@ -3428,7 +3483,7 @@ bool SharedMnCreateDialog::selectCoins(CAmount target, CoinSelection& selection,
return false;
}
const CAmount change{selection.total - target};
selection.change = (change > 0 && !is_dust(change)) ? change : 0;
selection.change = (change > 0 && !isDust(change)) ? change : 0;
return true;
}

Expand Down Expand Up @@ -3475,28 +3530,37 @@ void SharedMnCreateDialog::reserveCoins()
return;
}

CoinSelection selection;
if (!selectCoins(contributionTarget(), selection, error)) {
showError(error);
return;
}
// Whoever replied before a release holds the contribution as it was then,
// and refuses locked terms with any other. While its coins are still free
// and fund the same amount, reserve exactly that contribution again.
MnShareSession::Contribution contribution;
contribution.label = share.label;
contribution.inputs = selection.inputs;
if (selection.change > 0) {
const QString change_address{MasternodeWidgetUtil::freshAddress(m_wallet_model, error)};
if (change_address.isEmpty()) {
const auto* const again{reservableAgain()};
if (again != nullptr) {
contribution = *again;
} else {
CoinSelection selection;
if (!selectCoins(contributionTarget(), selection, error)) {
showError(error);
return;
}
contribution.hasChange = true;
contribution.changeAddress = change_address;
contribution.changeAmount = selection.change;
contribution.label = share.label;
contribution.inputs = selection.inputs;
if (selection.change > 0) {
const QString change_address{MasternodeWidgetUtil::freshAddress(m_wallet_model, error)};
if (change_address.isEmpty()) {
showError(error);
return;
}
contribution.hasChange = true;
contribution.changeAddress = change_address;
contribution.changeAmount = selection.change;
}
}
if (!m_session.addContribution(contribution, error)) {
showError(error);
return;
}
if (again == nullptr) m_built_contributions.push_back(contribution);
setContributionLocked(contribution, /*lock=*/true);
m_coins_released = false;
// Coins reserved again are no longer released
Expand Down
14 changes: 14 additions & 0 deletions src/qt/sharedmncreatedialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <memory>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>

struct SharedMnOwnFunding;
Expand Down Expand Up @@ -274,6 +275,16 @@ private Q_SLOTS:
void followNewerInvitation(const MnShareSession& imported);

// Funding
//! Whether a P2PKH output of `value` would be dust
bool isDust(CAmount value) const;
//! This wallet's unspent, unlocked, spendable coins that may fund a share
std::vector<std::pair<CAmount, COutPoint>> fundingCandidates() const;
//! Whether `released`'s coins are all still funding candidates, and fund
//! exactly what the share needs now
bool fundsShareAgain(const MnShareSession::Contribution& released, CAmount* total_out = nullptr) const;
//! The most recently released contribution of this wallet's share that
//! Reserve would put back as it was, with its input total, or nullptr
const MnShareSession::Contribution* reservableAgain(CAmount* total_out = nullptr) const;
//! Confirmed, unlocked, spendable wallet coins covering `target`, largest
//! first, with the change kept above the dust threshold when possible
bool selectCoins(CAmount target, CoinSelection& selection, QString& error) const;
Expand Down Expand Up @@ -409,6 +420,9 @@ private Q_SLOTS:
//! less any coins reserved again since. A reply to an earlier invitation
//! still carries them, and only Reserve may put them back.
std::vector<MnShareSession::Contribution> m_released_contributions;
//! Contributions Reserve has reserved, built from this wallet's own coin
//! selection or put back: the only ones it may put back after a release
std::vector<MnShareSession::Contribution> m_built_contributions;
Role m_role{Role::Undecided};
int m_my_share{-1};
std::optional<Page> m_validation_page;
Expand Down
Loading
Loading