Files
pay/Wallet_support.cpp
T

364 lines
14 KiB
C++
Raw Permalink Normal View History

/*
* This file is part of the Flowee project
2022-03-22 23:15:16 +01:00
* Copyright (C) 2020-2022 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/>.
*/
2021-11-02 14:24:47 +01:00
#include <QString>
#include "Wallet.h"
#include "Wallet_p.h"
2021-11-02 14:24:47 +01:00
#include "FloweePay.h"
#include "TransactionInfo.h"
#include <cashaddr.h>
#include <QTimer>
#include <QThread>
2021-11-02 14:24:47 +01:00
#include <primitives/script.h>
2022-01-24 12:31:49 +01:00
#include <streaming/BufferPools.h>
2021-11-02 14:24:47 +01:00
namespace {
// helper method for sortTransactions
void copyTx(size_t index, std::deque<Tx> &answer, const std::vector<QSet<int> > &order, std::vector<bool> &done, const std::deque<Tx> &in) {
if (done.at(index))
return;
done[index] = true;
for (auto dependencies : order.at(index)) {
copyTx(dependencies, answer, order, done, in);
}
answer.push_back(in.at(index));
}
QString renderAddress(const Streaming::ConstBuffer &outputScript)
{
std::vector<std::vector<uint8_t> > vSolutions;
Script::TxnOutType whichType;
if (!Script::solver(outputScript, whichType, vSolutions))
return QString();
2022-03-22 23:15:16 +01:00
switch (whichType) {
case Script::TX_PUBKEY:
2022-07-06 22:06:58 +02:00
return renderAddress(PublicKey(vSolutions[0]).getKeyId());
case Script::TX_PUBKEYHASH:
2022-07-06 22:06:58 +02:00
return renderAddress(KeyId(uint160(vSolutions[0])));
2022-03-22 23:15:16 +01:00
case Script::TX_SCRIPTHASH: {
CashAddress::Content c;
c.type = CashAddress::SCRIPT_TYPE;
c.hash = vSolutions[0];
2022-06-23 13:57:44 +02:00
auto s = CashAddress::encodeCashAddr(chainPrefix(), c);
const auto size = chainPrefix().size();
2022-03-22 23:15:16 +01:00
return QString::fromLatin1(s.c_str() + size + 1, s.size() - size -1); // the 1 is for the colon
}
default:
return QString();
}
}
2022-03-22 23:15:16 +01:00
} // anon namespace
std::deque<Tx> WalletPriv::sortTransactions(const std::deque<Tx> &in) {
if (in.size() < 2)
return in;
boost::unordered_map<uint256, int, HashShortener> txHashes;
for (size_t i = 0; i < in.size(); ++i) {
txHashes.insert(std::make_pair(in.at(i).createHash(), int(i)));
}
std::vector<QSet<int> > order;
order.reserve(in.size());
for (size_t i = 0; i < in.size(); ++i) {
Tx::Iterator iter(in.at(i));
QSet<int> depends;
for (auto input : Tx::findInputs(iter)) {
auto prev = txHashes.find(input.txid);
if (prev != txHashes.end()) {
depends.insert(prev->second);
}
}
order.push_back(depends);
}
std::vector<bool> done(in.size(), false);
std::deque<Tx> answer;
for (size_t i = 0; i < in.size(); ++i) {
copyTx(i, answer, order, done, in);
}
return answer;
}
// ////////////////////////////////////////////////////
Wallet::OutputRef::OutputRef(uint64_t encoded)
{
m_outputIndex = encoded & 0xFFFF;
encoded >>= 16;
assert(encoded < 0xFFFFFFFF);
m_txid = encoded & 0x7FFFFFFF;
assert(m_txid >= 0);
}
Wallet::OutputRef::OutputRef(int txIndex, int outputIndex)
: m_txid(txIndex),
m_outputIndex(outputIndex)
{
assert(txIndex >= 0); // zero is the 'invalid' state, which is allowed here to make an invalid OutputRef
assert(outputIndex >= 0);
assert(outputIndex <= 0XFFFF);
}
uint64_t Wallet::OutputRef::encoded() const
{
uint64_t answer = m_txid;
answer <<= 16;
answer += m_outputIndex;
return answer;
}
// //////////////////////////////////////////////////
WalletInfoObject::WalletInfoObject(Wallet *wallet, int txIndex, const Tx &tx)
: BroadcastTxData(tx),
m_wallet(wallet),
m_txIndex(txIndex)
{
connect(this, SIGNAL(finished(int,bool)),
m_wallet, SLOT(broadcastTxFinished(int,bool)), Qt::QueuedConnection);
}
void WalletInfoObject::txRejected(RejectReason reason, const std::string &message)
{
// reason is hinted using BroadcastTxData::RejectReason
logCritical() << "Transaction rejected" << reason << message;
++m_rejectedPeerCount;
}
void WalletInfoObject::sentOne()
{
if (++m_sentPeerCount >= 2) {
// Two stage singleshots. First (Qt requires that to be zero ms) to move
// to a thread that Qt owns.
// Second (in startCheckState()) to actually wait several seconds.
QTimer::singleShot(0, this, SLOT(startCheckState()));
}
}
void WalletInfoObject::startCheckState()
{
Q_ASSERT(thread() == QThread::currentThread());
QTimer::singleShot(5 * 1000, this, SLOT(checkState()));
}
void WalletInfoObject::checkState()
{
Q_ASSERT(thread() == QThread::currentThread());
if (m_sentPeerCount >= 2) {
emit finished(m_txIndex, m_sentPeerCount - m_rejectedPeerCount >= 1);
}
}
int WalletInfoObject::txIndex() const
{
return m_txIndex;
}
uint16_t WalletInfoObject::privSegment() const
{
assert(m_wallet);
assert(m_wallet->segment());
return m_wallet->segment()->segmentId();
}
2021-10-29 12:42:31 +02:00
2021-11-09 20:49:37 +01:00
// //////////////////////////////////////////////////
2021-11-09 20:49:37 +01:00
bool Wallet::WalletTransaction::isUnconfirmed() const
{
return minedBlockHeight == WalletPriv::Unconfirmed;
}
bool Wallet::WalletTransaction::isRejected() const
{
return minedBlockHeight == WalletPriv::Rejected;
}
2021-11-02 14:24:47 +01:00
// //////////////////////////////////////////////////
void Wallet::fetchTransactionInfo(TransactionInfo *info, int txIndex)
{
Q_ASSERT(info);
QMutexLocker locker(&m_lock);
auto iter = m_walletTransactions.find(txIndex);
if (m_walletTransactions.end() == iter)
throw std::runtime_error("Invalid tx-index");
2021-11-16 15:01:02 +01:00
const auto &wtx = iter->second;
info->m_isCashFusion = wtx.isCashFusionTx;
info->m_isCoinbase = wtx.isCoinbase;
info->m_userComment = wtx.userComment;
2022-01-24 12:31:49 +01:00
Tx tx = loadTransaction(wtx.txid, Streaming::pool(0));
info->m_txSize = tx.size();
2021-12-11 15:11:13 +01:00
/*
* The QML to show these lists of inputs and outputs depends on
* the quality of the column layout component. It turns out that
* in an older version of Qt this simple didn't work and all items
* are shown on top of each other.
* This "fixes" that problem by simply not generating the data
* to show in the UI.
* Known problem was in Ubuntu 20.04 (LTS) with Qt 5.12.8
*/
const auto qtVersion = QString(qVersion()).split('.');
assert(qtVersion.size() == 3);
if (qtVersion.at(0).toInt() <= 5 && qtVersion.at(1).toInt() < 15)
return;
// find out how many inputs and how many outputs there are.
Tx::Iterator txIter(tx);
// If we created this transaction (we have inputs in it anyway) then
// also look up all the outputs from the file.
2021-12-11 15:11:13 +01:00
// This creates a false-positive for tx we co-created (flipstarter etc), we already
// check for cash-fusion.
2021-11-16 15:01:02 +01:00
const bool createdByUs = !wtx.inputToWTX.empty() && !wtx.isCashFusionTx;
2021-12-04 20:09:43 +01:00
info->m_createdByUs = createdByUs;
do {
switch (txIter.next(Tx::PrevTxHash | Tx::OutputValue | Tx::OutputScript)) {
case Tx::PrevTxHash: info->m_inputs.append(nullptr); break;
case Tx::OutputValue: {
TransactionOutputInfo *out = nullptr;
if (createdByUs) {
out = new TransactionOutputInfo(info);
out->setForMe(false);
out->setValue(txIter.longData());
}
info->m_outputs.append(out);
break;
}
case Tx::OutputScript:
assert(!info->m_outputs.isEmpty());
if (createdByUs) {
assert(info->m_outputs.back());
info->m_outputs.back()->setAddress(renderAddress(txIter.byteData()));
}
break;
default: break; // silence compiler warnings
}
} while (txIter.tag() != Tx::End);
// probably only a couple of the inputs and outputs I have knowledge about,
// since only those that use our addresses are stored in the wallet.
// We find those and put the info objects in the assigned places.
2021-11-16 15:01:02 +01:00
for (auto pair : wtx.inputToWTX) {
OutputRef ref(pair.second);
auto w = m_walletTransactions.find(ref.txIndex());
assert(w != m_walletTransactions.end());
auto prevOut = w->second.outputs.find(ref.outputIndex());
assert(prevOut != w->second.outputs.end());
auto in = new TransactionInputInfo(info);
in->setValue(prevOut->second.value);
auto secretIter = m_walletSecrets.find(prevOut->second.walletSecretId);
assert(secretIter != m_walletSecrets.end());
const auto &secret = secretIter->second;
in->setAddress(renderAddress(secret.address));
if (secret.fromChangeChain) {
assert(secret.fromHdWallet);
in->setCloakedAddress(tr("Change #%1").arg(secret.hdDerivationIndex));
}
info->m_inputs[pair.first] = in;
}
// same for outputs
2021-11-16 15:01:02 +01:00
for (auto o : wtx.outputs) {
auto secretIter = m_walletSecrets.find(o.second.walletSecretId);
assert(secretIter != m_walletSecrets.end());
const auto &secret = secretIter->second;
TransactionOutputInfo *out;
if (createdByUs) { // reuse the one we created before from the raw Td.
out = info->m_outputs[o.first];
out->setForMe(true);
}
else {
out = new TransactionOutputInfo(info);
out->setValue(o.second.value);
out->setAddress(renderAddress(secret.address));
}
out->setSpent(m_unspentOutputs.find(OutputRef(txIndex, o.first).encoded()) == m_unspentOutputs.end());
if (secret.fromChangeChain) {
assert(secret.fromHdWallet);
out->setCloakedAddress(tr("Change #%1").arg(secret.hdDerivationIndex));
}
info->m_outputs[o.first] = out;
}
}
2021-11-02 14:24:47 +01:00
// Apologies for the nested loop design, I promise this is not representative code and should only ever be ran once per wallet, ever.
void Wallet::populateSigType()
{
const auto &txs = m_walletTransactions; // short alias for readability
logCritical().nospace() << "Upgrading wallet '" << m_name << "', Finding signature types from seen transactions";
// iterate though each private key
for (auto s = m_walletSecrets.begin(); s != m_walletSecrets.end(); ++s) {
auto &secret = s->second;
// logDebug() << "Secret" << s->first << "Hd:" << secret.fromHdWallet << secret.fromChangeChain << "index:" << secret.hdDerivationIndex;
// iterate through transactions and outputs to find one that deposited funds there.
for (auto t1 = txs.cbegin(); secret.signatureType == NotUsedYet && t1 != txs.cend(); ++t1) {
const auto &tx1 = t1->second;
for (auto o = tx1.outputs.cbegin(); secret.signatureType == NotUsedYet && o != tx1.outputs.cend(); ++o) {
if (o->second.walletSecretId == s->first) {
// logDebug() << " Found an out for secret" << t1->first << o->first;
const auto ref = OutputRef(t1->first, o->first).encoded();
// check UTXO, if still there, then its unspent.
// Unspent means no signature, so find another output to check.
if (m_unspentOutputs.find(ref) != m_unspentOutputs.end())
continue;
// iterate through transactions and inputs and try to find the spending input.
for (auto t2 = txs.cbegin(); secret.signatureType == NotUsedYet && t2 != txs.cend(); ++t2) {
const auto &tx2 = t2->second;
for (auto i = tx2.inputToWTX.cbegin(); i != tx2.inputToWTX.cend(); ++i) {
if (i->second == ref) {
// found one, now fetch the input script and check the type.
// logDebug() << "Found tx2 which spends output. Tx2:" << t2->first << i->first;
2022-01-24 12:31:49 +01:00
Tx tx = loadTransaction(tx2.txid, Streaming::pool(0));
2021-11-02 14:24:47 +01:00
Tx::Iterator txIter(tx);
for (int x = i->first; x >= 0; --x) {
if (txIter.next(Tx::TxInScript) != Tx::TxInScript) {
logCritical() << "Internal error; tx has too little inputs" << tx2.txid << i->first;
return;
}
}
assert(txIter.tag() == Tx::TxInScript);
CScript script(txIter.byteData());
auto scriptIter = script.begin();
opcodetype type;
script.GetOp(scriptIter, type);
secret.signatureType = type == 65 ? SignedAsSchnorr : SignedAsEcdsa;
logInfo() << "Secret key" << s->first << "detected signature type from tx-out:"
<< tx1.txid << o->first
<< "signed by tx-input:" << tx2.txid << i->first
<< "SigType:" << (type == 65 ? "SignedAsSchnorr" : "SignedAsEcdsa");
m_secretsChanged = true;
break;
}
}
}
}
}
}
}
logCritical() << "Wallet upgrade finished";
}