From 766e8a40b478353a89622f42809ddb11e695a0c9 Mon Sep 17 00:00:00 2001 From: Suhas Daftuar Date: Fri, 9 Dec 2016 15:31:06 -0500 Subject: [wallet] Add IsAllFromMe: true if all inputs are from wallet --- src/wallet/wallet.cpp | 23 +++++++++++++++++++++++ src/wallet/wallet.h | 6 ++++++ 2 files changed, 29 insertions(+) (limited to 'src') diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 2775f4def3..be5d692c92 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1154,6 +1154,8 @@ isminetype CWallet::IsMine(const CTxIn &txin) const return ISMINE_NO; } +// Note that this function doesn't distinguish between a 0-valued input, +// and a not-"is mine" (according to the filter) input. CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const { { @@ -1236,6 +1238,27 @@ CAmount CWallet::GetDebit(const CTransaction& tx, const isminefilter& filter) co return nDebit; } +bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const +{ + LOCK(cs_wallet); + + BOOST_FOREACH(const CTxIn& txin, tx.vin) + { + auto mi = mapWallet.find(txin.prevout.hash); + if (mi == mapWallet.end()) + return false; // any unknown inputs can't be from us + + const CWalletTx& prev = (*mi).second; + + if (txin.prevout.n >= prev.tx->vout.size()) + return false; // invalid input! + + if (!(IsMine(prev.tx->vout[txin.prevout.n]) & filter)) + return false; + } + return true; +} + CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const { CAmount nCredit = 0; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b9fa6bb24b..8a587f4ed0 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -825,6 +825,10 @@ public: std::set GetAccountAddresses(const std::string& strAccount) const; isminetype IsMine(const CTxIn& txin) const; + /** + * Returns amount of debit if the input matches the + * filter, otherwise returns 0 + */ CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const; isminetype IsMine(const CTxOut& txout) const; CAmount GetCredit(const CTxOut& txout, const isminefilter& filter) const; @@ -834,6 +838,8 @@ public: /** should probably be renamed to IsRelevantToMe */ bool IsFromMe(const CTransaction& tx) const; CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const; + /** Returns whether all of the inputs match the filter */ + bool IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const; CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const; CAmount GetChange(const CTransaction& tx) const; void SetBestChain(const CBlockLocator& loc); -- cgit v1.2.3 From 52dde66770d833ee5e42e7c5fee610453ae3852a Mon Sep 17 00:00:00 2001 From: Russell Yanofsky Date: Tue, 17 Jan 2017 10:40:41 -0500 Subject: [wallet] Add include_unsafe argument to listunspent RPC --- src/rpc/server.cpp | 14 +++++++++----- src/rpc/server.h | 5 +++++ src/wallet/rpcwallet.cpp | 31 ++++++++++++++++++++++--------- 3 files changed, 36 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 1b94e10071..283d458c8d 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -79,16 +79,20 @@ void RPCTypeCheck(const UniValue& params, break; const UniValue& v = params[i]; - if (!((v.type() == t) || (fAllowNull && (v.isNull())))) - { - string err = strprintf("Expected type %s, got %s", - uvTypeName(t), uvTypeName(v.type())); - throw JSONRPCError(RPC_TYPE_ERROR, err); + if (!(fAllowNull && v.isNull())) { + RPCTypeCheckArgument(v, t); } i++; } } +void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected) +{ + if (value.type() != typeExpected) { + throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Expected type %s, got %s", uvTypeName(typeExpected), uvTypeName(value.type()))); + } +} + void RPCTypeCheckObj(const UniValue& o, const map& typesExpected, bool fAllowNull, diff --git a/src/rpc/server.h b/src/rpc/server.h index fed3d8c90f..52f82866dc 100644 --- a/src/rpc/server.h +++ b/src/rpc/server.h @@ -78,6 +78,11 @@ bool RPCIsInWarmup(std::string *statusOut); void RPCTypeCheck(const UniValue& params, const std::list& typesExpected, bool fAllowNull=false); +/** + * Type-check one argument; throws JSONRPCError if wrong type given. + */ +void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected); + /* Check for expected keys/value types in an Object. */ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 4dda480216..ffe0827bef 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2350,9 +2350,9 @@ UniValue listunspent(const JSONRPCRequest& request) if (!EnsureWalletIsAvailable(request.fHelp)) return NullUniValue; - if (request.fHelp || request.params.size() > 3) + if (request.fHelp || request.params.size() > 4) throw runtime_error( - "listunspent ( minconf maxconf [\"addresses\",...] )\n" + "listunspent ( minconf maxconf [\"addresses\",...] [include_unsafe] )\n" "\nReturns array of unspent transaction outputs\n" "with between minconf and maxconf (inclusive) confirmations.\n" "Optionally filter to only include txouts paid to specified addresses.\n" @@ -2364,6 +2364,10 @@ UniValue listunspent(const JSONRPCRequest& request) " \"address\" (string) bitcoin address\n" " ,...\n" " ]\n" + "4. include_unsafe (bool, optional, default=true) Include outputs that are not safe to spend\n" + " because they come from unconfirmed untrusted transactions or unconfirmed\n" + " replacement transactions (cases where we are less sure that a conflicting\n" + " transaction won't be mined).\n" "\nResult\n" "[ (array of json object)\n" " {\n" @@ -2387,18 +2391,21 @@ UniValue listunspent(const JSONRPCRequest& request) + HelpExampleRpc("listunspent", "6, 9999999 \"[\\\"1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\",\\\"1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"") ); - RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VARR)); - int nMinDepth = 1; - if (request.params.size() > 0) + if (request.params.size() > 0 && !request.params[0].isNull()) { + RPCTypeCheckArgument(request.params[0], UniValue::VNUM); nMinDepth = request.params[0].get_int(); + } int nMaxDepth = 9999999; - if (request.params.size() > 1) + if (request.params.size() > 1 && !request.params[1].isNull()) { + RPCTypeCheckArgument(request.params[1], UniValue::VNUM); nMaxDepth = request.params[1].get_int(); + } set setAddress; - if (request.params.size() > 2) { + if (request.params.size() > 2 && !request.params[2].isNull()) { + RPCTypeCheckArgument(request.params[2], UniValue::VARR); UniValue inputs = request.params[2].get_array(); for (unsigned int idx = 0; idx < inputs.size(); idx++) { const UniValue& input = inputs[idx]; @@ -2411,11 +2418,17 @@ UniValue listunspent(const JSONRPCRequest& request) } } + bool include_unsafe = true; + if (request.params.size() > 3 && !request.params[3].isNull()) { + RPCTypeCheckArgument(request.params[3], UniValue::VBOOL); + include_unsafe = request.params[3].get_bool(); + } + UniValue results(UniValue::VARR); vector vecOutputs; assert(pwalletMain != NULL); LOCK2(cs_main, pwalletMain->cs_wallet); - pwalletMain->AvailableCoins(vecOutputs, false, NULL, true); + pwalletMain->AvailableCoins(vecOutputs, !include_unsafe, NULL, true); BOOST_FOREACH(const COutput& out, vecOutputs) { if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth) continue; @@ -2629,7 +2642,7 @@ static const CRPCCommand commands[] = { "wallet", "listreceivedbyaddress", &listreceivedbyaddress, false, {"minconf","include_empty","include_watchonly"} }, { "wallet", "listsinceblock", &listsinceblock, false, {"blockhash","target_confirmations","include_watchonly"} }, { "wallet", "listtransactions", &listtransactions, false, {"account","count","skip","include_watchonly"} }, - { "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses"} }, + { "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses","include_unsafe"} }, { "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} }, { "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} }, { "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, -- cgit v1.2.3 From cc0243ad32cee1cc9faab317364b889beaf07647 Mon Sep 17 00:00:00 2001 From: mrbandrews Date: Fri, 9 Dec 2016 13:45:27 -0500 Subject: [RPC] bumpfee This command allows a user to increase the fee on a wallet transaction T, creating a "bumper" transaction B. T must signal that it is BIP-125 replaceable. T's change output is decremented to pay the additional fee. (B will not add inputs to T.) T cannot have any descendant transactions. Once B bumps T, neither T nor B's outputs can be spent until either T or (more likely) B is mined. Includes code by @jonasschnelli and @ryanofsky --- src/rpc/client.cpp | 1 + src/wallet/rpcwallet.cpp | 258 +++++++++++++++++++++++++++++++++++++++++++++++ src/wallet/wallet.cpp | 67 ++++++++++++ src/wallet/wallet.h | 6 ++ 4 files changed, 332 insertions(+) (limited to 'src') diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 422d005f0c..28d24b84fd 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -116,6 +116,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "setnetworkactive", 0, "state" }, { "getmempoolancestors", 1, "verbose" }, { "getmempooldescendants", 1, "verbose" }, + { "bumpfee", 1, "options" }, // Echo with conversion (For testing only) { "echojson", 0, "arg0" }, { "echojson", 1, "arg1" }, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index ffe0827bef..dc2c6d292e 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -11,8 +11,10 @@ #include "init.h" #include "validation.h" #include "net.h" +#include "policy/policy.h" #include "policy/rbf.h" #include "rpc/server.h" +#include "script/sign.h" #include "timedata.h" #include "util.h" #include "utilmoneystr.h" @@ -2595,6 +2597,261 @@ UniValue fundrawtransaction(const JSONRPCRequest& request) return result; } +UniValue bumpfee(const JSONRPCRequest& request) +{ + if (!EnsureWalletIsAvailable(request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) { + throw runtime_error( + "bumpfee \"txid\" ( options ) \n" + "\nBumps the fee of an opt-in-RBF transaction T, replacing it with a new transaction B.\n" + "An opt-in RBF transaction with the given txid must be in the wallet.\n" + "The command will pay the additional fee by decreasing (or perhaps removing) its change output.\n" + "If the change output is not big enough to cover the increased fee, the command will currently fail\n" + "instead of adding new inputs to compensate. (A future implementation could improve this.)\n" + "The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n" + "By default, the new fee will be calculated automatically using estimatefee.\n" + "The user can specify a confirmation target for estimatefee.\n" + "Alternatively, the user can specify totalFee, or use RPC setpaytxfee to set a higher fee rate.\n" + "At a minimum, the new fee rate must be high enough to pay a new relay fee (relay fee amount returned\n" + "by getnetworkinfo RPC) and to enter the node's mempool.\n" + "\nArguments:\n" + "1. txid (string, required) The txid to be bumped\n" + "2. options (object, optional)\n" + " {\n" + " \"confTarget\" (numeric, optional) Confirmation target (in blocks)\n" + " \"totalFee\" (numeric, optional) Total fee (NOT feerate) to pay, in satoshis.\n" + " In rare cases, the actual fee paid might be slightly higher than the specified\n" + " totalFee if the tx change output has to be removed because it is too close to\n" + " the dust threshold.\n" + " \"replaceable\" (boolean, optional, default true) Whether the new transaction should still be\n" + " marked bip-125 replaceable. If true, the sequence numbers in the transaction will\n" + " be left unchanged from the original. If false, any input sequence numbers in the\n" + " original transaction that were less than 0xfffffffe will be increased to 0xfffffffe\n" + " so the new transaction will not be explicitly bip-125 replaceable (though it may\n" + " still be replacable in practice, for example if it has unconfirmed ancestors which\n" + " are replaceable).\n" + " }\n" + "\nResult:\n" + "{\n" + " \"txid\": \"value\", (string) The id of the new transaction\n" + " \"oldfee\": n, (numeric) Fee of the replaced transaction\n" + " \"fee\": n, (numeric) Fee of the new transaction\n" + "}\n" + "\nExamples:\n" + "\nBump the fee, get the new transaction\'s txid\n" + + HelpExampleCli("bumpfee", "")); + } + + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VOBJ)); + uint256 hash; + hash.SetHex(request.params[0].get_str()); + + // retrieve the original tx from the wallet + LOCK2(cs_main, pwalletMain->cs_wallet); + EnsureWalletIsUnlocked(); + if (!pwalletMain->mapWallet.count(hash)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + } + CWalletTx& wtx = pwalletMain->mapWallet[hash]; + + if (pwalletMain->HasWalletSpend(hash)) { + throw JSONRPCError(RPC_MISC_ERROR, "Transaction has descendants in the wallet"); + } + + { + LOCK(mempool.cs); + auto it = mempool.mapTx.find(hash); + if (it != mempool.mapTx.end() && it->GetCountWithDescendants() > 1) { + throw JSONRPCError(RPC_MISC_ERROR, "Transaction has descendants in the mempool"); + } + } + + if (wtx.GetDepthInMainChain() != 0) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction has been mined, or is conflicted with a mined transaction"); + } + + if (!SignalsOptInRBF(wtx)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction is not BIP 125 replaceable"); + } + + if (wtx.mapValue.count("replaced_by_txid")) { + throw JSONRPCError(RPC_INVALID_REQUEST, strprintf("Cannot bump transaction %s which was already bumped by transaction %s", hash.ToString(), wtx.mapValue.at("replaced_by_txid"))); + } + + // 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 (!pwalletMain->IsAllFromMe(wtx, ISMINE_SPENDABLE)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction contains inputs that don't belong to this wallet"); + } + + // figure out which output was change + // if there was no change output or multiple change outputs, fail + int nOutput = -1; + for (size_t i = 0; i < wtx.tx->vout.size(); ++i) { + if (pwalletMain->IsChange(wtx.tx->vout[i])) { + if (nOutput != -1) { + throw JSONRPCError(RPC_MISC_ERROR, "Transaction has multiple change outputs"); + } + nOutput = i; + } + } + if (nOutput == -1) { + throw JSONRPCError(RPC_MISC_ERROR, "Transaction does not have a change output"); + } + + // optional parameters + bool specifiedConfirmTarget = false; + int newConfirmTarget = nTxConfirmTarget; + CAmount totalFee = 0; + bool replaceable = true; + if (request.params.size() > 1) { + UniValue options = request.params[1]; + RPCTypeCheckObj(options, + { + {"confTarget", UniValueType(UniValue::VNUM)}, + {"totalFee", UniValueType(UniValue::VNUM)}, + {"replaceable", UniValueType(UniValue::VBOOL)}, + }, + true, true); + + if (options.exists("confTarget") && options.exists("totalFee")) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "confTarget and totalFee options should not both be set. Please provide either a confirmation target for fee estimation or an explicit total fee for the transaction."); + } else if (options.exists("confTarget")) { + specifiedConfirmTarget = true; + newConfirmTarget = options["confTarget"].get_int(); + if (newConfirmTarget <= 0) { // upper-bound will be checked by estimatefee/smartfee + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid confTarget (cannot be <= 0)"); + } + } else if (options.exists("totalFee")) { + totalFee = options["totalFee"].get_int64(); + if (totalFee <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid totalFee (cannot be <= 0)"); + } else if (totalFee > maxTxFee) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid totalFee (cannot be higher than maxTxFee)"); + } + } + + if (options.exists("replaceable")) { + replaceable = options["replaceable"].get_bool(); + } + } + + // signature sizes can vary by a byte, so add 1 for each input when calculating the new fee + int64_t txSize = GetVirtualTransactionSize(*(wtx.tx)); + const int64_t maxNewTxSize = txSize + wtx.tx->vin.size(); + + // calculate the old fee and fee-rate + CAmount nOldFee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut(); + CFeeRate nOldFeeRate(nOldFee, txSize); + CAmount nNewFee; + CFeeRate nNewFeeRate; + + if (totalFee > 0) { + CAmount minTotalFee = nOldFeeRate.GetFee(maxNewTxSize) + minRelayTxFee.GetFee(maxNewTxSize); + if (totalFee < minTotalFee) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid totalFee, must be at least %s (oldFee %s + relayFee %s)", FormatMoney(minTotalFee), nOldFeeRate.GetFee(maxNewTxSize), minRelayTxFee.GetFee(maxNewTxSize))); + } + nNewFee = totalFee; + nNewFeeRate = CFeeRate(totalFee, maxNewTxSize); + } else { + // use the user-defined payTxFee if possible, otherwise use smartfee / fallbackfee + if (!specifiedConfirmTarget && payTxFee.GetFeePerK() != 0) { + nNewFeeRate = payTxFee; + } else { + nNewFeeRate = mempool.estimateSmartFee(newConfirmTarget); + } + if (nNewFeeRate.GetFeePerK() == 0) { + nNewFeeRate = CWallet::fallbackFee; + } + + // new fee rate must be at least old rate + minimum relay rate + if (nNewFeeRate.GetFeePerK() < nOldFeeRate.GetFeePerK() + ::minRelayTxFee.GetFeePerK()) { + nNewFeeRate = CFeeRate(nOldFeeRate.GetFeePerK() + ::minRelayTxFee.GetFeePerK()); + } + + nNewFee = nNewFeeRate.GetFee(maxNewTxSize); + } + + // 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) + // 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 + // moment earlier. In this case, we report an error to the user, who may use totalFee to make an adjustment. + CFeeRate minMempoolFeeRate = mempool.GetMinFee(GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000); + if (nNewFeeRate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) { + throw JSONRPCError(RPC_MISC_ERROR, strprintf("New fee rate (%s) is less than the minimum fee rate (%s) to get into the mempool. totalFee value should to be at least %s or settxfee value should be at least %s to add transaction.", FormatMoney(nNewFeeRate.GetFeePerK()), FormatMoney(minMempoolFeeRate.GetFeePerK()), FormatMoney(minMempoolFeeRate.GetFee(maxNewTxSize)), FormatMoney(minMempoolFeeRate.GetFeePerK()))); + } + + // Now modify the output to increase the fee. + // If the output is not large enough to pay the fee, fail. + CAmount nDelta = nNewFee - nOldFee; + assert(nDelta > 0); + CMutableTransaction tx(*(wtx.tx)); + CTxOut* poutput = &(tx.vout[nOutput]); + if (poutput->nValue < nDelta) { + throw JSONRPCError(RPC_MISC_ERROR, "Change output is too small to bump the fee"); + } + + // If the output would become dust, discard it (converting the dust to fee) + poutput->nValue -= nDelta; + if (poutput->nValue <= poutput->GetDustThreshold(::minRelayTxFee)) { + LogPrint("rpc", "Bumping fee and discarding dust output\n"); + nNewFee += poutput->nValue; + tx.vout.erase(tx.vout.begin() + nOutput); + } + + // Mark new tx not replaceable, if requested. + if (!replaceable) { + for (auto& input : tx.vin) { + if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe; + } + } + + // sign the new tx + CTransaction txNewConst(tx); + int nIn = 0; + for (auto& input : tx.vin) { + std::map::const_iterator mi = pwalletMain->mapWallet.find(input.prevout.hash); + assert(mi != pwalletMain->mapWallet.end() && input.prevout.n < mi->second.tx->vout.size()); + const CScript& scriptPubKey = mi->second.tx->vout[input.prevout.n].scriptPubKey; + const CAmount& amount = mi->second.tx->vout[input.prevout.n].nValue; + SignatureData sigdata; + if (!ProduceSignature(TransactionSignatureCreator(pwalletMain, &txNewConst, nIn, amount, SIGHASH_ALL), scriptPubKey, sigdata)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Can't sign transaction."); + } + UpdateTransaction(tx, nIn, sigdata); + nIn++; + } + + // commit/broadcast the tx + CReserveKey reservekey(pwalletMain); + CWalletTx wtxBumped(pwalletMain, MakeTransactionRef(std::move(tx))); + wtxBumped.mapValue["replaces_txid"] = hash.ToString(); + CValidationState state; + if (!pwalletMain->CommitTransaction(wtxBumped, reservekey, g_connman.get(), state) || !state.IsValid()) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error: The transaction was rejected! Reason given: %s", state.GetRejectReason())); + } + + // mark the original tx as bumped + if (!pwalletMain->MarkReplaced(wtx.GetHash(), wtxBumped.GetHash())) { + // 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 + // wtxBumped to the caller even if marking the original transaction + // replaced does not succeed for some reason. + throw JSONRPCError(RPC_WALLET_ERROR, "Error: Created new bumpfee transaction but could not mark the original transaction as replaced."); + } + + UniValue result(UniValue::VOBJ); + result.push_back(Pair("txid", wtxBumped.GetHash().GetHex())); + result.push_back(Pair("oldfee", ValueFromAmount(nOldFee))); + result.push_back(Pair("fee", ValueFromAmount(nNewFee))); + + return result; +} + extern UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp extern UniValue importprivkey(const JSONRPCRequest& request); extern UniValue importaddress(const JSONRPCRequest& request); @@ -2614,6 +2871,7 @@ static const CRPCCommand commands[] = { "wallet", "addmultisigaddress", &addmultisigaddress, true, {"nrequired","keys","account"} }, { "wallet", "addwitnessaddress", &addwitnessaddress, true, {"address"} }, { "wallet", "backupwallet", &backupwallet, true, {"destination"} }, + { "wallet", "bumpfee", &bumpfee, true, {"txid", "options"} }, { "wallet", "dumpprivkey", &dumpprivkey, true, {"address"} }, { "wallet", "dumpwallet", &dumpwallet, true, {"filename"} }, { "wallet", "encryptwallet", &encryptwallet, true, {"passphrase"} }, diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index be5d692c92..c1e1f2d7ea 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -411,6 +411,13 @@ set CWallet::GetConflicts(const uint256& txid) const return result; } +bool CWallet::HasWalletSpend(const uint256& txid) const +{ + AssertLockHeld(cs_wallet); + auto iter = mapTxSpends.lower_bound(COutPoint(txid, 0)); + return (iter != mapTxSpends.end() && iter->first.hash == txid); +} + void CWallet::Flush(bool shutdown) { bitdb.Flush(shutdown); @@ -826,6 +833,35 @@ void CWallet::MarkDirty() } } +bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash) +{ + LOCK(cs_wallet); + + auto mi = mapWallet.find(originalHash); + + // There is a bug if MarkReplaced is not called on an existing wallet transaction. + assert(mi != mapWallet.end()); + + CWalletTx& wtx = (*mi).second; + + // Ensure for now that we're not overwriting data + assert(wtx.mapValue.count("replaced_by_txid") == 0); + + wtx.mapValue["replaced_by_txid"] = newHash.ToString(); + + CWalletDB walletdb(strWalletFile, "r+"); + + bool success = true; + if (!walletdb.WriteTx(wtx)) { + LogPrintf("%s: Updating walletdb tx %s failed", __func__, wtx.GetHash().ToString()); + success = false; + } + + NotifyTransactionChanged(this, originalHash, CT_UPDATED); + + return success; +} + bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose) { LOCK(cs_wallet); @@ -1981,6 +2017,37 @@ void CWallet::AvailableCoins(vector& vCoins, bool fOnlyConfirmed, const if (nDepth == 0 && !pcoin->InMempool()) continue; + // We should not consider coins from transactions that are replacing + // other transactions. + // + // Example: There is a transaction A which is replaced by bumpfee + // transaction B. In this case, we want to prevent creation of + // a transaction B' which spends an output of B. + // + // Reason: If transaction A were initially confirmed, transactions B + // and B' would no longer be valid, so the user would have to create + // a new transaction C to replace B'. However, in the case of a + // one-block reorg, transactions B' and C might BOTH be accepted, + // when the user only wanted one of them. Specifically, there could + // be a 1-block reorg away from the chain where transactions A and C + // were accepted to another chain where B, B', and C were all + // accepted. + if (nDepth == 0 && fOnlyConfirmed && pcoin->mapValue.count("replaces_txid")) { + continue; + } + + // Similarly, we should not consider coins from transactions that + // have been replaced. In the example above, we would want to prevent + // creation of a transaction A' spending an output of A, because if + // transaction B were initially confirmed, conflicting with A and + // A', we wouldn't want to the user to create a transaction D + // intending to replace A', but potentially resulting in a scenario + // where A, A', and D could all be accepted (instead of just B and + // D, or just A and A' like the user would want). + if (nDepth == 0 && fOnlyConfirmed && pcoin->mapValue.count("replaced_by_txid")) { + continue; + } + for (unsigned int i = 0; i < pcoin->tx->vout.size(); i++) { isminetype mine = IsMine(pcoin->tx->vout[i]); if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO && diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 8a587f4ed0..50c5025c24 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -891,6 +891,9 @@ public: //! Get wallet transactions that conflict with given transaction (spend same outputs) std::set GetConflicts(const uint256& txid) const; + //! Check if a given transaction has any of its outputs spent by another transaction in the wallet + bool HasWalletSpend(const uint256& txid) const; + //! Flush wallet (bitdb flush) void Flush(bool shutdown=false); @@ -927,6 +930,9 @@ public: /* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */ bool AbandonTransaction(const uint256& hashTx); + /** Mark a transaction as replaced by another transaction (e.g., BIP 125). */ + bool MarkReplaced(const uint256& originalHash, const uint256& newHash); + /* Returns the wallets help message */ static std::string GetWalletHelpString(bool showDebug); -- cgit v1.2.3