Browse Source

Merge #10600: Make feebumper class stateless

aed1d90ac [wallet] Change feebumper from class to functions (Russell Yanofsky)
37bdcca3c [refactor] Make feebumper namespace (Russell Yanofsky)
7c4f00919 [trivial] Rename feebumper variables according to project code style (Russell Yanofsky)

Pull request description:

  Make feebumper methods static and remove stored state in the class.

  Having the results of feebumper calls persist in an object makes process
  separation between Qt and wallet awkward, because it means the feebumper object
  either has to be serialized back and forth between Qt and wallet processes
  between fee bump calls, or that the feebumper object needs to stay alive in the
  wallet process with an object reference passed back to Qt. It's simpler just to
  have fee bumper calls return their results immediately instead of storing them
  in an object with an extended lifetime.

  In addition to making feebumper methods static, also:

  - Move LOCK calls from Qt code to feebumper
  - Move TransactionCanBeBumped implementation from Qt code to feebumper
  - Rename CFeeBumper class to FeeBumper (every CFeeBumper reference had to be
    updated in this PR anyway so this doesn't increase the size of the diff)

  This change was originally part of https://github.com/bitcoin/bitcoin/pull/10244

Tree-SHA512: bf75e0c741b4e9c8912e66cc1dedf0ff715f77ea65fc33f7020d97d9099b0f6448f5852236dac63eea649de7d6fc03b0b21492e2c5140fb7560a39cf085506fd
0.16
MarcoFalke 7 years ago
parent
commit
4ed818060e
No known key found for this signature in database
GPG Key ID: D2EA4850E7528B25
  1. 46
      src/qt/walletmodel.cpp
  2. 210
      src/wallet/feebumper.cpp
  3. 67
      src/wallet/feebumper.h
  4. 51
      src/wallet/rpcwallet.cpp

46
src/qt/walletmodel.cpp

@ -659,45 +659,39 @@ bool WalletModel::abandonTransaction(uint256 hash) const
bool WalletModel::transactionCanBeBumped(uint256 hash) const bool WalletModel::transactionCanBeBumped(uint256 hash) const
{ {
LOCK2(cs_main, wallet->cs_wallet); return feebumper::TransactionCanBeBumped(wallet, hash);
const CWalletTx *wtx = wallet->GetWalletTx(hash);
return wtx && SignalsOptInRBF(*(wtx->tx)) && !wtx->mapValue.count("replaced_by_txid");
} }
bool WalletModel::bumpFee(uint256 hash) bool WalletModel::bumpFee(uint256 hash)
{ {
std::unique_ptr<CFeeBumper> feeBump; CCoinControl coin_control;
{ coin_control.signalRbf = true;
CCoinControl coin_control; std::vector<std::string> errors;
coin_control.signalRbf = true; CAmount old_fee;
LOCK2(cs_main, wallet->cs_wallet); CAmount new_fee;
feeBump.reset(new CFeeBumper(wallet, hash, coin_control, 0)); CMutableTransaction mtx;
} if (feebumper::CreateTransaction(wallet, hash, coin_control, 0 /* totalFee */, errors, old_fee, new_fee, mtx) != feebumper::Result::OK) {
if (feeBump->getResult() != BumpFeeResult::OK)
{
QMessageBox::critical(0, tr("Fee bump error"), tr("Increasing transaction fee failed") + "<br />(" + QMessageBox::critical(0, tr("Fee bump error"), tr("Increasing transaction fee failed") + "<br />(" +
(feeBump->getErrors().size() ? QString::fromStdString(feeBump->getErrors()[0]) : "") +")"); (errors.size() ? QString::fromStdString(errors[0]) : "") +")");
return false; return false;
} }
// allow a user based fee verification // allow a user based fee verification
QString questionString = tr("Do you want to increase the fee?"); QString questionString = tr("Do you want to increase the fee?");
questionString.append("<br />"); questionString.append("<br />");
CAmount oldFee = feeBump->getOldFee();
CAmount newFee = feeBump->getNewFee();
questionString.append("<table style=\"text-align: left;\">"); questionString.append("<table style=\"text-align: left;\">");
questionString.append("<tr><td>"); questionString.append("<tr><td>");
questionString.append(tr("Current fee:")); questionString.append(tr("Current fee:"));
questionString.append("</td><td>"); questionString.append("</td><td>");
questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), oldFee)); questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), old_fee));
questionString.append("</td></tr><tr><td>"); questionString.append("</td></tr><tr><td>");
questionString.append(tr("Increase:")); questionString.append(tr("Increase:"));
questionString.append("</td><td>"); questionString.append("</td><td>");
questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), newFee - oldFee)); questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee - old_fee));
questionString.append("</td></tr><tr><td>"); questionString.append("</td></tr><tr><td>");
questionString.append(tr("New fee:")); questionString.append(tr("New fee:"));
questionString.append("</td><td>"); questionString.append("</td><td>");
questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), newFee)); questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee));
questionString.append("</td></tr></table>"); questionString.append("</td></tr></table>");
SendConfirmationDialog confirmationDialog(tr("Confirm fee bump"), questionString); SendConfirmationDialog confirmationDialog(tr("Confirm fee bump"), questionString);
confirmationDialog.exec(); confirmationDialog.exec();
@ -715,23 +709,15 @@ bool WalletModel::bumpFee(uint256 hash)
} }
// sign bumped transaction // sign bumped transaction
bool res = false; if (!feebumper::SignTransaction(wallet, mtx)) {
{
LOCK2(cs_main, wallet->cs_wallet);
res = feeBump->signTransaction(wallet);
}
if (!res) {
QMessageBox::critical(0, tr("Fee bump error"), tr("Can't sign transaction.")); QMessageBox::critical(0, tr("Fee bump error"), tr("Can't sign transaction."));
return false; return false;
} }
// commit the bumped transaction // commit the bumped transaction
{ uint256 txid;
LOCK2(cs_main, wallet->cs_wallet); if (feebumper::CommitTransaction(wallet, hash, std::move(mtx), errors, txid) != feebumper::Result::OK) {
res = feeBump->commit(wallet);
}
if(!res) {
QMessageBox::critical(0, tr("Fee bump error"), tr("Could not commit transaction") + "<br />(" + QMessageBox::critical(0, tr("Fee bump error"), tr("Could not commit transaction") + "<br />(" +
QString::fromStdString(feeBump->getErrors()[0])+")"); QString::fromStdString(errors[0])+")");
return false; return false;
} }
return true; return true;

210
src/wallet/feebumper.cpp

@ -23,7 +23,7 @@
// calculation, but we should be able to refactor after priority is removed). // calculation, but we should be able to refactor after priority is removed).
// NOTE: this requires that all inputs must be in mapWallet (eg the tx should // NOTE: this requires that all inputs must be in mapWallet (eg the tx should
// be IsAllFromMe). // be IsAllFromMe).
int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *pWallet) static int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet)
{ {
CMutableTransaction txNew(tx); CMutableTransaction txNew(tx);
std::vector<CInputCoin> vCoins; std::vector<CInputCoin> vCoins;
@ -31,11 +31,11 @@ int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *pWal
// IsAllFromMe(ISMINE_SPENDABLE), so every input should already be in our // IsAllFromMe(ISMINE_SPENDABLE), so every input should already be in our
// wallet, with a valid index into the vout array. // wallet, with a valid index into the vout array.
for (auto& input : tx.vin) { for (auto& input : tx.vin) {
const auto mi = pWallet->mapWallet.find(input.prevout.hash); const auto mi = wallet->mapWallet.find(input.prevout.hash);
assert(mi != pWallet->mapWallet.end() && input.prevout.n < mi->second.tx->vout.size()); assert(mi != wallet->mapWallet.end() && input.prevout.n < mi->second.tx->vout.size());
vCoins.emplace_back(CInputCoin(&(mi->second), input.prevout.n)); vCoins.emplace_back(CInputCoin(&(mi->second), input.prevout.n));
} }
if (!pWallet->DummySignTx(txNew, vCoins)) { if (!wallet->DummySignTx(txNew, vCoins)) {
// This should never happen, because IsAllFromMe(ISMINE_SPENDABLE) // This should never happen, because IsAllFromMe(ISMINE_SPENDABLE)
// implies that we can sign for every input. // implies that we can sign for every input.
return -1; return -1;
@ -43,103 +43,102 @@ int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *pWal
return GetVirtualTransactionSize(txNew); return GetVirtualTransactionSize(txNew);
} }
bool CFeeBumper::preconditionChecks(const CWallet *pWallet, const CWalletTx& wtx) { //! Check whether transaction has descendant in wallet or mempool, or has been
if (pWallet->HasWalletSpend(wtx.GetHash())) { //! mined, or conflicts with a mined transaction. Return a feebumper::Result.
vErrors.push_back("Transaction has descendants in the wallet"); static feebumper::Result PreconditionChecks(const CWallet* wallet, const CWalletTx& wtx, std::vector<std::string>& errors)
currentResult = BumpFeeResult::INVALID_PARAMETER; {
return false; if (wallet->HasWalletSpend(wtx.GetHash())) {
errors.push_back("Transaction has descendants in the wallet");
return feebumper::Result::INVALID_PARAMETER;
} }
{ {
LOCK(mempool.cs); LOCK(mempool.cs);
auto it_mp = mempool.mapTx.find(wtx.GetHash()); auto it_mp = mempool.mapTx.find(wtx.GetHash());
if (it_mp != mempool.mapTx.end() && it_mp->GetCountWithDescendants() > 1) { if (it_mp != mempool.mapTx.end() && it_mp->GetCountWithDescendants() > 1) {
vErrors.push_back("Transaction has descendants in the mempool"); errors.push_back("Transaction has descendants in the mempool");
currentResult = BumpFeeResult::INVALID_PARAMETER; return feebumper::Result::INVALID_PARAMETER;
return false;
} }
} }
if (wtx.GetDepthInMainChain() != 0) { if (wtx.GetDepthInMainChain() != 0) {
vErrors.push_back("Transaction has been mined, or is conflicted with a mined transaction"); errors.push_back("Transaction has been mined, or is conflicted with a mined transaction");
currentResult = BumpFeeResult::WALLET_ERROR; return feebumper::Result::WALLET_ERROR;
return false;
} }
return true; return feebumper::Result::OK;
} }
CFeeBumper::CFeeBumper(const CWallet *pWallet, const uint256 txidIn, const CCoinControl& coin_control, CAmount totalFee) namespace feebumper {
:
txid(std::move(txidIn)), bool TransactionCanBeBumped(CWallet* wallet, const uint256& txid)
nOldFee(0),
nNewFee(0)
{ {
vErrors.clear(); LOCK2(cs_main, wallet->cs_wallet);
bumpedTxid.SetNull(); const CWalletTx* wtx = wallet->GetWalletTx(txid);
AssertLockHeld(pWallet->cs_wallet); return wtx && SignalsOptInRBF(*wtx->tx) && !wtx->mapValue.count("replaced_by_txid");
auto it = pWallet->mapWallet.find(txid); }
if (it == pWallet->mapWallet.end()) {
vErrors.push_back("Invalid or non-wallet transaction id"); Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoinControl& coin_control, CAmount total_fee, std::vector<std::string>& errors,
currentResult = BumpFeeResult::INVALID_ADDRESS_OR_KEY; CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx)
return; {
LOCK2(cs_main, wallet->cs_wallet);
errors.clear();
auto it = wallet->mapWallet.find(txid);
if (it == wallet->mapWallet.end()) {
errors.push_back("Invalid or non-wallet transaction id");
return Result::INVALID_ADDRESS_OR_KEY;
} }
const CWalletTx& wtx = it->second; const CWalletTx& wtx = it->second;
if (!preconditionChecks(pWallet, wtx)) { Result result = PreconditionChecks(wallet, wtx, errors);
return; if (result != Result::OK) {
return result;
} }
if (!SignalsOptInRBF(*wtx.tx)) { if (!SignalsOptInRBF(*wtx.tx)) {
vErrors.push_back("Transaction is not BIP 125 replaceable"); errors.push_back("Transaction is not BIP 125 replaceable");
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
if (wtx.mapValue.count("replaced_by_txid")) { if (wtx.mapValue.count("replaced_by_txid")) {
vErrors.push_back(strprintf("Cannot bump transaction %s which was already bumped by transaction %s", txid.ToString(), wtx.mapValue.at("replaced_by_txid"))); errors.push_back(strprintf("Cannot bump transaction %s which was already bumped by transaction %s", txid.ToString(), wtx.mapValue.at("replaced_by_txid")));
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
// check that original tx consists entirely of our inputs // check that original tx consists entirely of our inputs
// if not, we can't bump the fee, because the wallet has no way of knowing the value of the other inputs (thus the fee) // if not, we can't bump the fee, because the wallet has no way of knowing the value of the other inputs (thus the fee)
if (!pWallet->IsAllFromMe(*wtx.tx, ISMINE_SPENDABLE)) { if (!wallet->IsAllFromMe(*wtx.tx, ISMINE_SPENDABLE)) {
vErrors.push_back("Transaction contains inputs that don't belong to this wallet"); errors.push_back("Transaction contains inputs that don't belong to this wallet");
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
// figure out which output was change // figure out which output was change
// if there was no change output or multiple change outputs, fail // if there was no change output or multiple change outputs, fail
int nOutput = -1; int nOutput = -1;
for (size_t i = 0; i < wtx.tx->vout.size(); ++i) { for (size_t i = 0; i < wtx.tx->vout.size(); ++i) {
if (pWallet->IsChange(wtx.tx->vout[i])) { if (wallet->IsChange(wtx.tx->vout[i])) {
if (nOutput != -1) { if (nOutput != -1) {
vErrors.push_back("Transaction has multiple change outputs"); errors.push_back("Transaction has multiple change outputs");
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
nOutput = i; nOutput = i;
} }
} }
if (nOutput == -1) { if (nOutput == -1) {
vErrors.push_back("Transaction does not have a change output"); errors.push_back("Transaction does not have a change output");
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
// Calculate the expected size of the new transaction. // Calculate the expected size of the new transaction.
int64_t txSize = GetVirtualTransactionSize(*(wtx.tx)); int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
const int64_t maxNewTxSize = CalculateMaximumSignedTxSize(*wtx.tx, pWallet); const int64_t maxNewTxSize = CalculateMaximumSignedTxSize(*wtx.tx, wallet);
if (maxNewTxSize < 0) { if (maxNewTxSize < 0) {
vErrors.push_back("Transaction contains inputs that cannot be signed"); errors.push_back("Transaction contains inputs that cannot be signed");
currentResult = BumpFeeResult::INVALID_ADDRESS_OR_KEY; return Result::INVALID_ADDRESS_OR_KEY;
return;
} }
// calculate the old fee and fee-rate // calculate the old fee and fee-rate
nOldFee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut(); old_fee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
CFeeRate nOldFeeRate(nOldFee, txSize); CFeeRate nOldFeeRate(old_fee, txSize);
CFeeRate nNewFeeRate; CFeeRate nNewFeeRate;
// The wallet uses a conservative WALLET_INCREMENTAL_RELAY_FEE value to // The wallet uses a conservative WALLET_INCREMENTAL_RELAY_FEE value to
// future proof against changes to network wide policy for incremental relay // future proof against changes to network wide policy for incremental relay
@ -149,26 +148,24 @@ CFeeBumper::CFeeBumper(const CWallet *pWallet, const uint256 txidIn, const CCoin
walletIncrementalRelayFee = ::incrementalRelayFee; walletIncrementalRelayFee = ::incrementalRelayFee;
} }
if (totalFee > 0) { if (total_fee > 0) {
CAmount minTotalFee = nOldFeeRate.GetFee(maxNewTxSize) + ::incrementalRelayFee.GetFee(maxNewTxSize); CAmount minTotalFee = nOldFeeRate.GetFee(maxNewTxSize) + ::incrementalRelayFee.GetFee(maxNewTxSize);
if (totalFee < minTotalFee) { if (total_fee < minTotalFee) {
vErrors.push_back(strprintf("Insufficient totalFee, must be at least %s (oldFee %s + incrementalFee %s)", errors.push_back(strprintf("Insufficient totalFee, must be at least %s (oldFee %s + incrementalFee %s)",
FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxNewTxSize)), FormatMoney(::incrementalRelayFee.GetFee(maxNewTxSize)))); FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxNewTxSize)), FormatMoney(::incrementalRelayFee.GetFee(maxNewTxSize))));
currentResult = BumpFeeResult::INVALID_PARAMETER; return Result::INVALID_PARAMETER;
return;
} }
CAmount requiredFee = GetRequiredFee(maxNewTxSize); CAmount requiredFee = GetRequiredFee(maxNewTxSize);
if (totalFee < requiredFee) { if (total_fee < requiredFee) {
vErrors.push_back(strprintf("Insufficient totalFee (cannot be less than required fee %s)", errors.push_back(strprintf("Insufficient totalFee (cannot be less than required fee %s)",
FormatMoney(requiredFee))); FormatMoney(requiredFee)));
currentResult = BumpFeeResult::INVALID_PARAMETER; return Result::INVALID_PARAMETER;
return;
} }
nNewFee = totalFee; new_fee = total_fee;
nNewFeeRate = CFeeRate(totalFee, maxNewTxSize); nNewFeeRate = CFeeRate(total_fee, maxNewTxSize);
} else { } else {
nNewFee = GetMinimumFee(maxNewTxSize, coin_control, mempool, ::feeEstimator, nullptr /* FeeCalculation */); new_fee = GetMinimumFee(maxNewTxSize, coin_control, mempool, ::feeEstimator, nullptr /* FeeCalculation */);
nNewFeeRate = CFeeRate(nNewFee, maxNewTxSize); nNewFeeRate = CFeeRate(new_fee, maxNewTxSize);
// New fee rate must be at least old rate + minimum incremental relay rate // New fee rate must be at least old rate + minimum incremental relay rate
// walletIncrementalRelayFee.GetFeePerK() should be exact, because it's initialized // walletIncrementalRelayFee.GetFeePerK() should be exact, because it's initialized
@ -177,53 +174,50 @@ CFeeBumper::CFeeBumper(const CWallet *pWallet, const uint256 txidIn, const CCoin
// add 1 satoshi to the result, because it may have been rounded down. // add 1 satoshi to the result, because it may have been rounded down.
if (nNewFeeRate.GetFeePerK() < nOldFeeRate.GetFeePerK() + 1 + walletIncrementalRelayFee.GetFeePerK()) { if (nNewFeeRate.GetFeePerK() < nOldFeeRate.GetFeePerK() + 1 + walletIncrementalRelayFee.GetFeePerK()) {
nNewFeeRate = CFeeRate(nOldFeeRate.GetFeePerK() + 1 + walletIncrementalRelayFee.GetFeePerK()); nNewFeeRate = CFeeRate(nOldFeeRate.GetFeePerK() + 1 + walletIncrementalRelayFee.GetFeePerK());
nNewFee = nNewFeeRate.GetFee(maxNewTxSize); new_fee = nNewFeeRate.GetFee(maxNewTxSize);
} }
} }
// Check that in all cases the new fee doesn't violate maxTxFee // Check that in all cases the new fee doesn't violate maxTxFee
if (nNewFee > maxTxFee) { if (new_fee > maxTxFee) {
vErrors.push_back(strprintf("Specified or calculated fee %s is too high (cannot be higher than maxTxFee %s)", errors.push_back(strprintf("Specified or calculated fee %s is too high (cannot be higher than maxTxFee %s)",
FormatMoney(nNewFee), FormatMoney(maxTxFee))); FormatMoney(new_fee), FormatMoney(maxTxFee)));
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
// check that fee rate is higher than mempool's minimum fee // check that fee rate is higher than mempool's minimum fee
// (no point in bumping fee if we know that the new tx won't be accepted to the mempool) // (no point in bumping fee if we know that the new tx won't be accepted to the mempool)
// This may occur if the user set TotalFee or paytxfee too low, if fallbackfee is too low, or, perhaps, // This may occur if the user set TotalFee or paytxfee too low, if fallbackfee is too low, or, perhaps,
// in a rare situation where the mempool minimum fee increased significantly since the fee estimation just a // in a rare situation where the mempool minimum fee increased significantly since the fee estimation just a
// moment earlier. In this case, we report an error to the user, who may use totalFee to make an adjustment. // moment earlier. In this case, we report an error to the user, who may use total_fee to make an adjustment.
CFeeRate minMempoolFeeRate = mempool.GetMinFee(gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000); CFeeRate minMempoolFeeRate = mempool.GetMinFee(gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000);
if (nNewFeeRate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) { if (nNewFeeRate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) {
vErrors.push_back(strprintf( errors.push_back(strprintf(
"New fee rate (%s) is lower than the minimum fee rate (%s) to get into the mempool -- " "New fee rate (%s) is lower than the minimum fee rate (%s) to get into the mempool -- "
"the totalFee value should be at least %s or the settxfee value should be at least %s to add transaction", "the totalFee value should be at least %s or the settxfee value should be at least %s to add transaction",
FormatMoney(nNewFeeRate.GetFeePerK()), FormatMoney(nNewFeeRate.GetFeePerK()),
FormatMoney(minMempoolFeeRate.GetFeePerK()), FormatMoney(minMempoolFeeRate.GetFeePerK()),
FormatMoney(minMempoolFeeRate.GetFee(maxNewTxSize)), FormatMoney(minMempoolFeeRate.GetFee(maxNewTxSize)),
FormatMoney(minMempoolFeeRate.GetFeePerK()))); FormatMoney(minMempoolFeeRate.GetFeePerK())));
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
// Now modify the output to increase the fee. // Now modify the output to increase the fee.
// If the output is not large enough to pay the fee, fail. // If the output is not large enough to pay the fee, fail.
CAmount nDelta = nNewFee - nOldFee; CAmount nDelta = new_fee - old_fee;
assert(nDelta > 0); assert(nDelta > 0);
mtx = *wtx.tx; mtx = *wtx.tx;
CTxOut* poutput = &(mtx.vout[nOutput]); CTxOut* poutput = &(mtx.vout[nOutput]);
if (poutput->nValue < nDelta) { if (poutput->nValue < nDelta) {
vErrors.push_back("Change output is too small to bump the fee"); errors.push_back("Change output is too small to bump the fee");
currentResult = BumpFeeResult::WALLET_ERROR; return Result::WALLET_ERROR;
return;
} }
// If the output would become dust, discard it (converting the dust to fee) // If the output would become dust, discard it (converting the dust to fee)
poutput->nValue -= nDelta; poutput->nValue -= nDelta;
if (poutput->nValue <= GetDustThreshold(*poutput, ::dustRelayFee)) { if (poutput->nValue <= GetDustThreshold(*poutput, ::dustRelayFee)) {
LogPrint(BCLog::RPC, "Bumping fee and discarding dust output\n"); LogPrint(BCLog::RPC, "Bumping fee and discarding dust output\n");
nNewFee += poutput->nValue; new_fee += poutput->nValue;
mtx.vout.erase(mtx.vout.begin() + nOutput); mtx.vout.erase(mtx.vout.begin() + nOutput);
} }
@ -234,36 +228,36 @@ CFeeBumper::CFeeBumper(const CWallet *pWallet, const uint256 txidIn, const CCoin
} }
} }
currentResult = BumpFeeResult::OK; return Result::OK;
} }
bool CFeeBumper::signTransaction(CWallet *pWallet) bool SignTransaction(CWallet* wallet, CMutableTransaction& mtx) {
{ LOCK2(cs_main, wallet->cs_wallet);
return pWallet->SignTransaction(mtx); return wallet->SignTransaction(mtx);
} }
bool CFeeBumper::commit(CWallet *pWallet) Result CommitTransaction(CWallet* wallet, const uint256& txid, CMutableTransaction&& mtx, std::vector<std::string>& errors, uint256& bumped_txid)
{ {
AssertLockHeld(pWallet->cs_wallet); LOCK2(cs_main, wallet->cs_wallet);
if (!vErrors.empty() || currentResult != BumpFeeResult::OK) { if (!errors.empty()) {
return false; return Result::MISC_ERROR;
} }
auto it = txid.IsNull() ? pWallet->mapWallet.end() : pWallet->mapWallet.find(txid); auto it = txid.IsNull() ? wallet->mapWallet.end() : wallet->mapWallet.find(txid);
if (it == pWallet->mapWallet.end()) { if (it == wallet->mapWallet.end()) {
vErrors.push_back("Invalid or non-wallet transaction id"); errors.push_back("Invalid or non-wallet transaction id");
currentResult = BumpFeeResult::MISC_ERROR; return Result::MISC_ERROR;
return false;
} }
CWalletTx& oldWtx = it->second; CWalletTx& oldWtx = it->second;
// make sure the transaction still has no descendants and hasn't been mined in the meantime // make sure the transaction still has no descendants and hasn't been mined in the meantime
if (!preconditionChecks(pWallet, oldWtx)) { Result result = PreconditionChecks(wallet, oldWtx, errors);
return false; if (result != Result::OK) {
return result;
} }
CWalletTx wtxBumped(pWallet, MakeTransactionRef(std::move(mtx))); CWalletTx wtxBumped(wallet, MakeTransactionRef(std::move(mtx)));
// commit/broadcast the tx // commit/broadcast the tx
CReserveKey reservekey(pWallet); CReserveKey reservekey(wallet);
wtxBumped.mapValue = oldWtx.mapValue; wtxBumped.mapValue = oldWtx.mapValue;
wtxBumped.mapValue["replaces_txid"] = oldWtx.GetHash().ToString(); wtxBumped.mapValue["replaces_txid"] = oldWtx.GetHash().ToString();
wtxBumped.vOrderForm = oldWtx.vOrderForm; wtxBumped.vOrderForm = oldWtx.vOrderForm;
@ -271,27 +265,29 @@ bool CFeeBumper::commit(CWallet *pWallet)
wtxBumped.fTimeReceivedIsTxTime = true; wtxBumped.fTimeReceivedIsTxTime = true;
wtxBumped.fFromMe = true; wtxBumped.fFromMe = true;
CValidationState state; CValidationState state;
if (!pWallet->CommitTransaction(wtxBumped, reservekey, g_connman.get(), state)) { if (!wallet->CommitTransaction(wtxBumped, reservekey, g_connman.get(), state)) {
// NOTE: CommitTransaction never returns false, so this should never happen. // NOTE: CommitTransaction never returns false, so this should never happen.
vErrors.push_back(strprintf("The transaction was rejected: %s", state.GetRejectReason())); errors.push_back(strprintf("The transaction was rejected: %s", state.GetRejectReason()));
return false; return Result::WALLET_ERROR;
} }
bumpedTxid = wtxBumped.GetHash(); bumped_txid = wtxBumped.GetHash();
if (state.IsInvalid()) { if (state.IsInvalid()) {
// This can happen if the mempool rejected the transaction. Report // This can happen if the mempool rejected the transaction. Report
// what happened in the "errors" response. // what happened in the "errors" response.
vErrors.push_back(strprintf("The transaction was rejected: %s", FormatStateMessage(state))); errors.push_back(strprintf("Error: The transaction was rejected: %s", FormatStateMessage(state)));
} }
// mark the original tx as bumped // mark the original tx as bumped
if (!pWallet->MarkReplaced(oldWtx.GetHash(), wtxBumped.GetHash())) { if (!wallet->MarkReplaced(oldWtx.GetHash(), wtxBumped.GetHash())) {
// TODO: see if JSON-RPC has a standard way of returning a response // TODO: see if JSON-RPC has a standard way of returning a response
// along with an exception. It would be good to return information about // along with an exception. It would be good to return information about
// wtxBumped to the caller even if marking the original transaction // wtxBumped to the caller even if marking the original transaction
// replaced does not succeed for some reason. // replaced does not succeed for some reason.
vErrors.push_back("Created new bumpfee transaction but could not mark the original transaction as replaced"); errors.push_back("Created new bumpfee transaction but could not mark the original transaction as replaced");
} }
return true; return Result::OK;
} }
} // namespace feebumper

67
src/wallet/feebumper.h

@ -13,7 +13,9 @@ class uint256;
class CCoinControl; class CCoinControl;
enum class FeeEstimateMode; enum class FeeEstimateMode;
enum class BumpFeeResult namespace feebumper {
enum class Result
{ {
OK, OK,
INVALID_ADDRESS_OR_KEY, INVALID_ADDRESS_OR_KEY,
@ -23,39 +25,34 @@ enum class BumpFeeResult
MISC_ERROR, MISC_ERROR,
}; };
class CFeeBumper //! Return whether transaction can be bumped.
{ bool TransactionCanBeBumped(CWallet* wallet, const uint256& txid);
public:
CFeeBumper(const CWallet *pWalletIn, const uint256 txidIn, const CCoinControl& coin_control, CAmount totalFee); //! Create bumpfee transaction.
BumpFeeResult getResult() const { return currentResult; } Result CreateTransaction(const CWallet* wallet,
const std::vector<std::string>& getErrors() const { return vErrors; } const uint256& txid,
CAmount getOldFee() const { return nOldFee; } const CCoinControl& coin_control,
CAmount getNewFee() const { return nNewFee; } CAmount total_fee,
uint256 getBumpedTxId() const { return bumpedTxid; } std::vector<std::string>& errors,
CAmount& old_fee,
/* signs the new transaction, CAmount& new_fee,
* returns false if the tx couldn't be found or if it was CMutableTransaction& mtx);
* impossible to create the signature(s)
*/ //! Sign the new transaction,
bool signTransaction(CWallet *pWallet); //! @return false if the tx couldn't be found or if it was
//! impossible to create the signature(s)
/* commits the fee bump, bool SignTransaction(CWallet* wallet, CMutableTransaction& mtx);
* returns true, in case of CWallet::CommitTransaction was successful
* but, eventually sets vErrors if the tx could not be added to the mempool (will try later) //! Commit the bumpfee transaction.
* or if the old transaction could not be marked as replaced //! @return success in case of CWallet::CommitTransaction was successful,
*/ //! but sets errors if the tx could not be added to the mempool (will try later)
bool commit(CWallet *pWalletNonConst); //! or if the old transaction could not be marked as replaced.
Result CommitTransaction(CWallet* wallet,
private: const uint256& txid,
bool preconditionChecks(const CWallet *pWallet, const CWalletTx& wtx); CMutableTransaction&& mtx,
std::vector<std::string>& errors,
const uint256 txid; uint256& bumped_txid);
uint256 bumpedTxid;
CMutableTransaction mtx; } // namespace feebumper
std::vector<std::string> vErrors;
BumpFeeResult currentResult;
CAmount nOldFee;
CAmount nNewFee;
};
#endif // BITCOIN_WALLET_FEEBUMPER_H #endif // BITCOIN_WALLET_FEEBUMPER_H

51
src/wallet/rpcwallet.cpp

@ -3224,45 +3224,50 @@ UniValue bumpfee(const JSONRPCRequest& request)
LOCK2(cs_main, pwallet->cs_wallet); LOCK2(cs_main, pwallet->cs_wallet);
EnsureWalletIsUnlocked(pwallet); EnsureWalletIsUnlocked(pwallet);
CFeeBumper feeBump(pwallet, hash, coin_control, totalFee);
BumpFeeResult res = feeBump.getResult(); std::vector<std::string> errors;
if (res != BumpFeeResult::OK) CAmount old_fee;
{ CAmount new_fee;
CMutableTransaction mtx;
feebumper::Result res = feebumper::CreateTransaction(pwallet, hash, coin_control, totalFee, errors, old_fee, new_fee, mtx);
if (res != feebumper::Result::OK) {
switch(res) { switch(res) {
case BumpFeeResult::INVALID_ADDRESS_OR_KEY: case feebumper::Result::INVALID_ADDRESS_OR_KEY:
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, feeBump.getErrors()[0]); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, errors[0]);
break; break;
case BumpFeeResult::INVALID_REQUEST: case feebumper::Result::INVALID_REQUEST:
throw JSONRPCError(RPC_INVALID_REQUEST, feeBump.getErrors()[0]); throw JSONRPCError(RPC_INVALID_REQUEST, errors[0]);
break; break;
case BumpFeeResult::INVALID_PARAMETER: case feebumper::Result::INVALID_PARAMETER:
throw JSONRPCError(RPC_INVALID_PARAMETER, feeBump.getErrors()[0]); throw JSONRPCError(RPC_INVALID_PARAMETER, errors[0]);
break; break;
case BumpFeeResult::WALLET_ERROR: case feebumper::Result::WALLET_ERROR:
throw JSONRPCError(RPC_WALLET_ERROR, feeBump.getErrors()[0]); throw JSONRPCError(RPC_WALLET_ERROR, errors[0]);
break; break;
default: default:
throw JSONRPCError(RPC_MISC_ERROR, feeBump.getErrors()[0]); throw JSONRPCError(RPC_MISC_ERROR, errors[0]);
break; break;
} }
} }
// sign bumped transaction // sign bumped transaction
if (!feeBump.signTransaction(pwallet)) { if (!feebumper::SignTransaction(pwallet, mtx)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Can't sign transaction."); throw JSONRPCError(RPC_WALLET_ERROR, "Can't sign transaction.");
} }
// commit the bumped transaction // commit the bumped transaction
if(!feeBump.commit(pwallet)) { uint256 txid;
throw JSONRPCError(RPC_WALLET_ERROR, feeBump.getErrors()[0]); if (feebumper::CommitTransaction(pwallet, hash, std::move(mtx), errors, txid) != feebumper::Result::OK) {
throw JSONRPCError(RPC_WALLET_ERROR, errors[0]);
} }
UniValue result(UniValue::VOBJ); UniValue result(UniValue::VOBJ);
result.push_back(Pair("txid", feeBump.getBumpedTxId().GetHex())); result.push_back(Pair("txid", txid.GetHex()));
result.push_back(Pair("origfee", ValueFromAmount(feeBump.getOldFee()))); result.push_back(Pair("origfee", ValueFromAmount(old_fee)));
result.push_back(Pair("fee", ValueFromAmount(feeBump.getNewFee()))); result.push_back(Pair("fee", ValueFromAmount(new_fee)));
UniValue errors(UniValue::VARR); UniValue result_errors(UniValue::VARR);
for (const std::string& err: feeBump.getErrors()) for (const std::string& error : errors) {
errors.push_back(err); result_errors.push_back(error);
result.push_back(Pair("errors", errors)); }
result.push_back(Pair("errors", result_errors));
return result; return result;
} }

Loading…
Cancel
Save