Files
pay/Wallet_spending.cpp
T

190 lines
6.9 KiB
C++

/*
* This file is part of the Flowee project
* Copyright (C) 2020-2021 Tom Zander <tom@flowee.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Wallet.h"
#include "Wallet_p.h"
#include "FloweePay.h"
namespace {
struct UnspentOutput {
Wallet::OutputRef outputRef;
qint64 value = 0; // in satoshis
int score = 0; // the score gained by using this tx.
};
int scoreForSolution(size_t outputCount, int64_t change, size_t unspentOutputCount)
{
assert(unspentOutputCount > 0);
assert(outputCount > 0);
assert(change > 0);
const int resultingOutputCount = unspentOutputCount - outputCount;
int score = 0;
// aim to keep our output count between 10 and 40
if (resultingOutputCount > 10 && resultingOutputCount <= 50)
score = 1000; // perfection
else if (resultingOutputCount > 5 && resultingOutputCount < 40)
score = 250;
else if (resultingOutputCount < 25 && resultingOutputCount > 10)
score = 250;
else if (resultingOutputCount > 40)
score -= (resultingOutputCount - 40) * 5;
else if (resultingOutputCount <= 5)
score -= (5 - resultingOutputCount) * 40; // for the 0 - 5 range
// in most cases no modifier is added due to change
if (change < 100)
score += 2000; // thats very nice (if over 0, that's for the miner)
else if (change < 1000) // we would create very small UTXO, not nice.
score -= 1000;
else if (change < 5000) // ditto
score -= 800;
return score;
}
}
Wallet::OutputSet Wallet::findInputsFor(qint64 output, int feePerByte, int txSize, int64_t &change) const
{
/*
* The main selection criterea is the amount of outputs we have afterwards.
*
* The goal is to always have between 10 and 15 outputs of varying sizes in
* our wallet, this makes sure we avoid being without confirmed outputs. Even
* on medium-heavy usage.
*
*
* As we assume the first items in the list of unspentOutputs are the oldest, all
* we need to do is find the combination of inputs that works best.
*/
const int currentBlockHeight = FloweePay::instance()->headerChainHeight();
QList<UnspentOutput> unspentOutputs;
std::map<uint64_t, size_t> utxosBySize;
unspentOutputs.reserve(m_unspentOutputs.size());
for (auto iter = m_unspentOutputs.begin(); iter != m_unspentOutputs.end(); ++iter) {
if (m_lockedOutputs.find(iter->first) != m_lockedOutputs.end())
continue;
UnspentOutput out;
out.value = iter->second;
out.outputRef = OutputRef(iter->first);
auto wtxIter = m_walletTransactions.find(out.outputRef.txIndex());
Q_ASSERT(wtxIter != m_walletTransactions.end());
int h = wtxIter->second.minedBlockHeight;
if (h == WalletPriv::Unconfirmed) {
out.score = -10; // unconfirmed.
} else if (wtxIter->second.isCoinbase
&& h + MATURATION_AGE >= m_lastBlockHeightSeen) {
// don't spend an immature coinbase
continue;
} else {
const int diff = currentBlockHeight - h;
if (diff > 4024)
out.score = -20;
else if (diff > 1008)
out.score = -30;
else if (diff > 144)
out.score = -50;
}
utxosBySize.insert(std::make_pair(iter->second, unspentOutputs.size()));
unspentOutputs.push_back(out);
}
// First simply walk from oldest to newest until funded
OutputSet bestSet;
int bestScore = 0;
bestSet.fee = txSize * feePerByte;
for (auto iter = unspentOutputs.begin(); iter != unspentOutputs.end(); ++iter) {
bestSet.outputs.push_back(OutputRef(iter->outputRef));
bestSet.totalSats += iter->value;
bestSet.fee += BYTES_PER_OUTPUT * feePerByte;
bestScore += iter->score;
if (output != -1 && bestSet.totalSats - bestSet.fee >= output)
break;
}
if (output == -1) { // the magic number to return all outputs
change = 0;
return bestSet;
}
if (bestSet.totalSats - bestSet.fee < output)
return OutputSet();
bestScore += scoreForSolution(bestSet.outputs.size(),
bestSet.totalSats - bestSet.fee - output, unspentOutputs.size());
// try a new set.
OutputSet current;
int score = 0;
current.fee = txSize * feePerByte;
auto iterBySize = utxosBySize.end();
while (iterBySize != utxosBySize.begin()) {
--iterBySize;
const auto &utxo = unspentOutputs.at(iterBySize->second);
current.outputs.push_back(utxo.outputRef);
current.totalSats += utxo.value;
current.fee += BYTES_PER_OUTPUT * feePerByte;
score += utxo.score;
if (current.totalSats - current.fee >= output)
break;
}
if (current.totalSats - current.fee >= output) {
score += scoreForSolution(current.outputs.size(),
current.totalSats - current.fee - output, unspentOutputs.size());
// compare with the cost of oldest to newest.
if (score > bestScore) {
bestScore = score;
bestSet = current;
}
}
// Last we use random sets.
for (int setIndex = 0; setIndex < 50; ++setIndex) {
current = OutputSet();
score = 0;
current.fee = txSize * feePerByte;
auto outputs = unspentOutputs;
do {
Q_ASSERT(!outputs.empty());
const int index = static_cast<int>(rand() % outputs.size());
Q_ASSERT(outputs.size() > index);
const auto &out = outputs[index];
current.outputs.push_back(out.outputRef);
current.totalSats += out.value;
current.fee += BYTES_PER_OUTPUT * feePerByte;
score += out.score;
outputs.removeAt(index); // take it.
} while (current.totalSats - current.fee < output);
score += scoreForSolution(current.outputs.size(),
(current.totalSats - current.fee) - output, unspentOutputs.size());
Q_ASSERT(current.totalSats - current.fee >= output);
if (score > bestScore) {
bestScore = score;
bestSet = current;
}
}
change = bestSet.totalSats - bestSet.fee - output;
return bestSet;
}