diff options
-rw-r--r-- | src/Makefile.am | 3 | ||||
-rw-r--r-- | src/policy/fees.cpp | 11 | ||||
-rw-r--r-- | src/policy/fees.h | 40 | ||||
-rw-r--r-- | src/rpc/misc.cpp | 51 | ||||
-rw-r--r-- | src/rpc/net.cpp | 147 | ||||
-rw-r--r-- | src/txmempool.cpp | 12 | ||||
-rw-r--r-- | src/txmempool.h | 6 | ||||
-rw-r--r-- | src/validation.cpp | 6 | ||||
-rw-r--r-- | src/wallet/rpc/backup.cpp (renamed from src/wallet/rpcdump.cpp) | 105 | ||||
-rw-r--r-- | src/wallet/rpc/encrypt.cpp | 248 | ||||
-rw-r--r-- | src/wallet/rpc/util.cpp | 32 | ||||
-rw-r--r-- | src/wallet/rpc/util.h | 4 | ||||
-rw-r--r-- | src/wallet/rpcwallet.cpp | 384 | ||||
-rw-r--r-- | src/wallet/scriptpubkeyman.cpp | 36 | ||||
-rw-r--r-- | src/wallet/scriptpubkeyman.h | 28 | ||||
-rw-r--r-- | src/wallet/test/wallet_tests.cpp | 25 | ||||
-rw-r--r-- | src/wallet/wallet.cpp | 78 | ||||
-rw-r--r-- | src/wallet/wallet.h | 12 | ||||
-rwxr-xr-x | test/functional/wallet_listtransactions.py | 66 |
19 files changed, 741 insertions, 553 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 4a60ce8b90..72f548c192 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -410,9 +410,10 @@ libbitcoin_wallet_a_SOURCES = \ wallet/interfaces.cpp \ wallet/load.cpp \ wallet/receive.cpp \ + wallet/rpc/backup.cpp \ + wallet/rpc/encrypt.cpp \ wallet/rpc/signmessage.cpp \ wallet/rpc/util.cpp \ - wallet/rpcdump.cpp \ wallet/rpcwallet.cpp \ wallet/scriptpubkeyman.cpp \ wallet/spend.cpp \ diff --git a/src/policy/fees.cpp b/src/policy/fees.cpp index d8c21bd833..36cf786bd5 100644 --- a/src/policy/fees.cpp +++ b/src/policy/fees.cpp @@ -493,6 +493,12 @@ void TxConfirmStats::removeTx(unsigned int entryHeight, unsigned int nBestSeenHe bool CBlockPolicyEstimator::removeTx(uint256 hash, bool inBlock) { LOCK(m_cs_fee_estimator); + return _removeTx(hash, inBlock); +} + +bool CBlockPolicyEstimator::_removeTx(const uint256& hash, bool inBlock) +{ + AssertLockHeld(m_cs_fee_estimator); std::map<uint256, TxStatsInfo>::iterator pos = mapMemPoolTxs.find(hash); if (pos != mapMemPoolTxs.end()) { feeStats->removeTx(pos->second.blockHeight, nBestSeenHeight, pos->second.bucketIndex, inBlock); @@ -576,7 +582,8 @@ void CBlockPolicyEstimator::processTransaction(const CTxMemPoolEntry& entry, boo bool CBlockPolicyEstimator::processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry) { - if (!removeTx(entry->GetTx().GetHash(), true)) { + AssertLockHeld(m_cs_fee_estimator); + if (!_removeTx(entry->GetTx().GetHash(), true)) { // This transaction wasn't being tracked for fee estimation return false; } @@ -985,7 +992,7 @@ void CBlockPolicyEstimator::FlushUnconfirmed() { // Remove every entry in mapMemPoolTxs while (!mapMemPoolTxs.empty()) { auto mi = mapMemPoolTxs.begin(); - removeTx(mi->first, false); // this calls erase() on mapMemPoolTxs + _removeTx(mi->first, false); // this calls erase() on mapMemPoolTxs } int64_t endclear = GetTimeMicros(); LogPrint(BCLog::ESTIMATEFEE, "Recorded %u unconfirmed txs from mempool in %gs\n", num_entries, (endclear - startclear)*0.000001); diff --git a/src/policy/fees.h b/src/policy/fees.h index 27f9120c64..37a7051045 100644 --- a/src/policy/fees.h +++ b/src/policy/fees.h @@ -186,47 +186,59 @@ public: /** Process all the transactions that have been included in a block */ void processBlock(unsigned int nBlockHeight, - std::vector<const CTxMemPoolEntry*>& entries); + std::vector<const CTxMemPoolEntry*>& entries) + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Process a transaction accepted to the mempool*/ - void processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate); + void processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate) + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Remove a transaction from the mempool tracking stats*/ - bool removeTx(uint256 hash, bool inBlock); + bool removeTx(uint256 hash, bool inBlock) + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** DEPRECATED. Return a feerate estimate */ - CFeeRate estimateFee(int confTarget) const; + CFeeRate estimateFee(int confTarget) const + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Estimate feerate needed to get be included in a block within confTarget * blocks. If no answer can be given at confTarget, return an estimate at * the closest target where one can be given. 'conservative' estimates are * valid over longer time horizons also. */ - CFeeRate estimateSmartFee(int confTarget, FeeCalculation *feeCalc, bool conservative) const; + CFeeRate estimateSmartFee(int confTarget, FeeCalculation *feeCalc, bool conservative) const + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Return a specific fee estimate calculation with a given success * threshold and time horizon, and optionally return detailed data about * calculation */ - CFeeRate estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult *result = nullptr) const; + CFeeRate estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, + EstimationResult* result = nullptr) const + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Write estimation data to a file */ - bool Write(CAutoFile& fileout) const; + bool Write(CAutoFile& fileout) const + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Read estimation data from a file */ - bool Read(CAutoFile& filein); + bool Read(CAutoFile& filein) + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Empty mempool transactions on shutdown to record failure to confirm for txs still in mempool */ - void FlushUnconfirmed(); + void FlushUnconfirmed() + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Calculation of highest target that estimates are tracked for */ - unsigned int HighestTargetTracked(FeeEstimateHorizon horizon) const; + unsigned int HighestTargetTracked(FeeEstimateHorizon horizon) const + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Drop still unconfirmed transactions and record current estimations, if the fee estimation file is present. */ - void Flush(); + void Flush() + EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); private: - mutable RecursiveMutex m_cs_fee_estimator; + mutable Mutex m_cs_fee_estimator; unsigned int nBestSeenHeight GUARDED_BY(m_cs_fee_estimator); unsigned int firstRecordedHeight GUARDED_BY(m_cs_fee_estimator); @@ -267,6 +279,10 @@ private: unsigned int HistoricalBlockSpan() const EXCLUSIVE_LOCKS_REQUIRED(m_cs_fee_estimator); /** Calculation of highest target that reasonable estimate can be provided for */ unsigned int MaxUsableEstimate() const EXCLUSIVE_LOCKS_REQUIRED(m_cs_fee_estimator); + + /** A non-thread-safe helper for the removeTx function */ + bool _removeTx(const uint256& hash, bool inBlock) + EXCLUSIVE_LOCKS_REQUIRED(m_cs_fee_estimator); }; class FeeFilterRounder diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index 6a3e160d8c..2c05fc39fd 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -36,32 +36,33 @@ static RPCHelpMan validateaddress() { - return RPCHelpMan{"validateaddress", - "\nReturn information about the given bitcoin address.\n", - { - {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to validate"}, - }, - RPCResult{ - RPCResult::Type::OBJ, "", "", + return RPCHelpMan{ + "validateaddress", + "\nReturn information about the given bitcoin address.\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address to validate"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::BOOL, "isvalid", "If the address is valid or not"}, + {RPCResult::Type::STR, "address", /* optional */ true, "The bitcoin address validated"}, + {RPCResult::Type::STR_HEX, "scriptPubKey", /* optional */ true, "The hex-encoded scriptPubKey generated by the address"}, + {RPCResult::Type::BOOL, "isscript", /* optional */ true, "If the key is a script"}, + {RPCResult::Type::BOOL, "iswitness", /* optional */ true, "If the address is a witness address"}, + {RPCResult::Type::NUM, "witness_version", /* optional */ true, "The version number of the witness program"}, + {RPCResult::Type::STR_HEX, "witness_program", /* optional */ true, "The hex value of the witness program"}, + {RPCResult::Type::STR, "error", /* optional */ true, "Error message, if any"}, + {RPCResult::Type::ARR, "error_locations", /*optional=*/true, "Indices of likely error locations in address, if known (e.g. Bech32 errors)", { - {RPCResult::Type::BOOL, "isvalid", "If the address is valid or not"}, - {RPCResult::Type::STR, "address", /* optional */ true, "The bitcoin address validated"}, - {RPCResult::Type::STR_HEX, "scriptPubKey", /* optional */ true, "The hex-encoded scriptPubKey generated by the address"}, - {RPCResult::Type::BOOL, "isscript", /* optional */ true, "If the key is a script"}, - {RPCResult::Type::BOOL, "iswitness", /* optional */ true, "If the address is a witness address"}, - {RPCResult::Type::NUM, "witness_version", /* optional */ true, "The version number of the witness program"}, - {RPCResult::Type::STR_HEX, "witness_program", /* optional */ true, "The hex value of the witness program"}, - {RPCResult::Type::STR, "error", /* optional */ true, "Error message, if any"}, - {RPCResult::Type::ARR, "error_locations", "Indices of likely error locations in address, if known (e.g. Bech32 errors)", - { - {RPCResult::Type::NUM, "index", "index of a potential error"}, - }}, - } - }, - RPCExamples{ - HelpExampleCli("validateaddress", "\"" + EXAMPLE_ADDRESS[0] + "\"") + - HelpExampleRpc("validateaddress", "\"" + EXAMPLE_ADDRESS[0] + "\"") - }, + {RPCResult::Type::NUM, "index", "index of a potential error"}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("validateaddress", "\"" + EXAMPLE_ADDRESS[0] + "\"") + + HelpExampleRpc("validateaddress", "\"" + EXAMPLE_ADDRESS[0] + "\"") + }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { std::string error_msg; diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index f4456bebc5..021e6ae320 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -106,82 +106,83 @@ static RPCHelpMan ping() static RPCHelpMan getpeerinfo() { - return RPCHelpMan{"getpeerinfo", - "\nReturns data about each connected network node as a json array of objects.\n", - {}, - RPCResult{ - RPCResult::Type::ARR, "", "", + return RPCHelpMan{ + "getpeerinfo", + "\nReturns data about each connected network node as a json array of objects.\n", + {}, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::OBJ, "", "", + { { - {RPCResult::Type::OBJ, "", "", - { - { - {RPCResult::Type::NUM, "id", "Peer index"}, - {RPCResult::Type::STR, "addr", "(host:port) The IP address and port of the peer"}, - {RPCResult::Type::STR, "addrbind", /* optional */ true, "(ip:port) Bind address of the connection to the peer"}, - {RPCResult::Type::STR, "addrlocal", /* optional */ true, "(ip:port) Local address as reported by the peer"}, - {RPCResult::Type::STR, "network", "Network (" + Join(GetNetworkNames(/* append_unroutable */ true), ", ") + ")"}, - {RPCResult::Type::NUM, "mapped_as", /* optional */ true, "The AS in the BGP route to the peer used for diversifying\n" - "peer selection (only available if the asmap config flag is set)"}, - {RPCResult::Type::STR_HEX, "services", "The services offered"}, - {RPCResult::Type::ARR, "servicesnames", "the services offered, in human-readable form", - { - {RPCResult::Type::STR, "SERVICE_NAME", "the service name if it is recognised"} - }}, - {RPCResult::Type::BOOL, "relaytxes", "Whether peer has asked us to relay transactions to it"}, - {RPCResult::Type::NUM_TIME, "lastsend", "The " + UNIX_EPOCH_TIME + " of the last send"}, - {RPCResult::Type::NUM_TIME, "lastrecv", "The " + UNIX_EPOCH_TIME + " of the last receive"}, - {RPCResult::Type::NUM_TIME, "last_transaction", "The " + UNIX_EPOCH_TIME + " of the last valid transaction received from this peer"}, - {RPCResult::Type::NUM_TIME, "last_block", "The " + UNIX_EPOCH_TIME + " of the last block received from this peer"}, - {RPCResult::Type::NUM, "bytessent", "The total bytes sent"}, - {RPCResult::Type::NUM, "bytesrecv", "The total bytes received"}, - {RPCResult::Type::NUM_TIME, "conntime", "The " + UNIX_EPOCH_TIME + " of the connection"}, - {RPCResult::Type::NUM, "timeoffset", "The time offset in seconds"}, - {RPCResult::Type::NUM, "pingtime", /* optional */ true, "ping time (if available)"}, - {RPCResult::Type::NUM, "minping", /* optional */ true, "minimum observed ping time (if any at all)"}, - {RPCResult::Type::NUM, "pingwait", /* optional */ true, "ping wait (if non-zero)"}, - {RPCResult::Type::NUM, "version", "The peer version, such as 70001"}, - {RPCResult::Type::STR, "subver", "The string version"}, - {RPCResult::Type::BOOL, "inbound", "Inbound (true) or Outbound (false)"}, - {RPCResult::Type::BOOL, "bip152_hb_to", "Whether we selected peer as (compact blocks) high-bandwidth peer"}, - {RPCResult::Type::BOOL, "bip152_hb_from", "Whether peer selected us as (compact blocks) high-bandwidth peer"}, - {RPCResult::Type::NUM, "startingheight", "The starting height (block) of the peer"}, - {RPCResult::Type::NUM, "synced_headers", "The last header we have in common with this peer"}, - {RPCResult::Type::NUM, "synced_blocks", "The last block we have in common with this peer"}, - {RPCResult::Type::ARR, "inflight", "", - { - {RPCResult::Type::NUM, "n", "The heights of blocks we're currently asking from this peer"}, - }}, - {RPCResult::Type::BOOL, "addr_relay_enabled", "Whether we participate in address relay with this peer"}, - {RPCResult::Type::NUM, "addr_processed", "The total number of addresses processed, excluding those dropped due to rate limiting"}, - {RPCResult::Type::NUM, "addr_rate_limited", "The total number of addresses dropped due to rate limiting"}, - {RPCResult::Type::ARR, "permissions", "Any special permissions that have been granted to this peer", - { - {RPCResult::Type::STR, "permission_type", Join(NET_PERMISSIONS_DOC, ",\n") + ".\n"}, - }}, - {RPCResult::Type::NUM, "minfeefilter", "The minimum fee rate for transactions this peer accepts"}, - {RPCResult::Type::OBJ_DYN, "bytessent_per_msg", "", - { - {RPCResult::Type::NUM, "msg", "The total bytes sent aggregated by message type\n" - "When a message type is not listed in this json object, the bytes sent are 0.\n" - "Only known message types can appear as keys in the object."} - }}, - {RPCResult::Type::OBJ_DYN, "bytesrecv_per_msg", "", - { - {RPCResult::Type::NUM, "msg", "The total bytes received aggregated by message type\n" - "When a message type is not listed in this json object, the bytes received are 0.\n" - "Only known message types can appear as keys in the object and all bytes received\n" - "of unknown message types are listed under '"+NET_MESSAGE_COMMAND_OTHER+"'."} - }}, - {RPCResult::Type::STR, "connection_type", "Type of connection: \n" + Join(CONNECTION_TYPE_DOC, ",\n") + ".\n" - "Please note this output is unlikely to be stable in upcoming releases as we iterate to\n" - "best capture connection behaviors."}, - }}, + {RPCResult::Type::NUM, "id", "Peer index"}, + {RPCResult::Type::STR, "addr", "(host:port) The IP address and port of the peer"}, + {RPCResult::Type::STR, "addrbind", /* optional */ true, "(ip:port) Bind address of the connection to the peer"}, + {RPCResult::Type::STR, "addrlocal", /* optional */ true, "(ip:port) Local address as reported by the peer"}, + {RPCResult::Type::STR, "network", "Network (" + Join(GetNetworkNames(/* append_unroutable */ true), ", ") + ")"}, + {RPCResult::Type::NUM, "mapped_as", /* optional */ true, "The AS in the BGP route to the peer used for diversifying\n" + "peer selection (only available if the asmap config flag is set)"}, + {RPCResult::Type::STR_HEX, "services", "The services offered"}, + {RPCResult::Type::ARR, "servicesnames", "the services offered, in human-readable form", + { + {RPCResult::Type::STR, "SERVICE_NAME", "the service name if it is recognised"} }}, - }, - RPCExamples{ - HelpExampleCli("getpeerinfo", "") + {RPCResult::Type::BOOL, "relaytxes", "Whether peer has asked us to relay transactions to it"}, + {RPCResult::Type::NUM_TIME, "lastsend", "The " + UNIX_EPOCH_TIME + " of the last send"}, + {RPCResult::Type::NUM_TIME, "lastrecv", "The " + UNIX_EPOCH_TIME + " of the last receive"}, + {RPCResult::Type::NUM_TIME, "last_transaction", "The " + UNIX_EPOCH_TIME + " of the last valid transaction received from this peer"}, + {RPCResult::Type::NUM_TIME, "last_block", "The " + UNIX_EPOCH_TIME + " of the last block received from this peer"}, + {RPCResult::Type::NUM, "bytessent", "The total bytes sent"}, + {RPCResult::Type::NUM, "bytesrecv", "The total bytes received"}, + {RPCResult::Type::NUM_TIME, "conntime", "The " + UNIX_EPOCH_TIME + " of the connection"}, + {RPCResult::Type::NUM, "timeoffset", "The time offset in seconds"}, + {RPCResult::Type::NUM, "pingtime", /* optional */ true, "ping time (if available)"}, + {RPCResult::Type::NUM, "minping", /* optional */ true, "minimum observed ping time (if any at all)"}, + {RPCResult::Type::NUM, "pingwait", /* optional */ true, "ping wait (if non-zero)"}, + {RPCResult::Type::NUM, "version", "The peer version, such as 70001"}, + {RPCResult::Type::STR, "subver", "The string version"}, + {RPCResult::Type::BOOL, "inbound", "Inbound (true) or Outbound (false)"}, + {RPCResult::Type::BOOL, "bip152_hb_to", "Whether we selected peer as (compact blocks) high-bandwidth peer"}, + {RPCResult::Type::BOOL, "bip152_hb_from", "Whether peer selected us as (compact blocks) high-bandwidth peer"}, + {RPCResult::Type::NUM, "startingheight", /*optional=*/true, "The starting height (block) of the peer"}, + {RPCResult::Type::NUM, "synced_headers", /*optional=*/true, "The last header we have in common with this peer"}, + {RPCResult::Type::NUM, "synced_blocks", /*optional=*/true, "The last block we have in common with this peer"}, + {RPCResult::Type::ARR, "inflight", /*optional=*/true, "", + { + {RPCResult::Type::NUM, "n", "The heights of blocks we're currently asking from this peer"}, + }}, + {RPCResult::Type::BOOL, "addr_relay_enabled", /*optional=*/true, "Whether we participate in address relay with this peer"}, + {RPCResult::Type::NUM, "addr_processed", /*optional=*/true, "The total number of addresses processed, excluding those dropped due to rate limiting"}, + {RPCResult::Type::NUM, "addr_rate_limited", /*optional=*/true, "The total number of addresses dropped due to rate limiting"}, + {RPCResult::Type::ARR, "permissions", "Any special permissions that have been granted to this peer", + { + {RPCResult::Type::STR, "permission_type", Join(NET_PERMISSIONS_DOC, ",\n") + ".\n"}, + }}, + {RPCResult::Type::NUM, "minfeefilter", "The minimum fee rate for transactions this peer accepts"}, + {RPCResult::Type::OBJ_DYN, "bytessent_per_msg", "", + { + {RPCResult::Type::NUM, "msg", "The total bytes sent aggregated by message type\n" + "When a message type is not listed in this json object, the bytes sent are 0.\n" + "Only known message types can appear as keys in the object."} + }}, + {RPCResult::Type::OBJ_DYN, "bytesrecv_per_msg", "", + { + {RPCResult::Type::NUM, "msg", "The total bytes received aggregated by message type\n" + "When a message type is not listed in this json object, the bytes received are 0.\n" + "Only known message types can appear as keys in the object and all bytes received\n" + "of unknown message types are listed under '"+NET_MESSAGE_COMMAND_OTHER+"'."} + }}, + {RPCResult::Type::STR, "connection_type", "Type of connection: \n" + Join(CONNECTION_TYPE_DOC, ",\n") + ".\n" + "Please note this output is unlikely to be stable in upcoming releases as we iterate to\n" + "best capture connection behaviors."}, + }}, + }}, + }, + RPCExamples{ + HelpExampleCli("getpeerinfo", "") + HelpExampleRpc("getpeerinfo", "") - }, + }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { NodeContext& node = EnsureAnyNodeContext(request.context); diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 27fbb8acac..fcfc27d38e 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -5,6 +5,7 @@ #include <txmempool.h> +#include <chain.h> #include <coins.h> #include <consensus/consensus.h> #include <consensus/tx_verify.h> @@ -73,16 +74,15 @@ private: const LockPoints& lp; }; -bool TestLockPointValidity(CChain& active_chain, const LockPoints* lp) +bool TestLockPointValidity(CChain& active_chain, const LockPoints& lp) { AssertLockHeld(cs_main); - assert(lp); // If there are relative lock times then the maxInputBlock will be set // If there are no relative lock times, the LockPoints don't depend on the chain - if (lp->maxInputBlock) { + if (lp.maxInputBlock) { // Check whether active_chain is an extension of the block at which the LockPoints // calculation was valid. If not LockPoints are no longer valid - if (!active_chain.Contains(lp->maxInputBlock)) { + if (!active_chain.Contains(lp.maxInputBlock)) { return false; } } @@ -649,8 +649,8 @@ void CTxMemPool::removeForReorg(CChain& chain, std::function<bool(txiter)> check } RemoveStaged(setAllRemoves, false, MemPoolRemovalReason::REORG); for (indexed_transaction_set::const_iterator it = mapTx.begin(); it != mapTx.end(); it++) { - LockPoints lp = it->GetLockPoints(); - if (!TestLockPointValidity(chain, &lp)) { + const LockPoints lp{it->GetLockPoints()}; + if (!TestLockPointValidity(chain, lp)) { mapTx.modify(it, update_lock_points(lp)); } } diff --git a/src/txmempool.h b/src/txmempool.h index c6e08a3ca5..f87ecc9cd0 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -14,7 +14,6 @@ #include <utility> #include <vector> -#include <chain.h> #include <coins.h> #include <consensus/amount.h> #include <indirectmap.h> @@ -26,12 +25,13 @@ #include <util/epochguard.h> #include <util/hasher.h> -#include <boost/multi_index_container.hpp> #include <boost/multi_index/hashed_index.hpp> #include <boost/multi_index/ordered_index.hpp> #include <boost/multi_index/sequenced_index.hpp> +#include <boost/multi_index_container.hpp> class CBlockIndex; +class CChain; class CChainState; extern RecursiveMutex cs_main; @@ -53,7 +53,7 @@ struct LockPoints { /** * Test whether the LockPoints height and time are still valid on the current chain */ -bool TestLockPointValidity(CChain& active_chain, const LockPoints* lp) EXCLUSIVE_LOCKS_REQUIRED(cs_main); +bool TestLockPointValidity(CChain& active_chain, const LockPoints& lp) EXCLUSIVE_LOCKS_REQUIRED(cs_main); struct CompareIteratorByHash { // SFINAE for T where T is either a pointer type (e.g., a txiter) or a reference_wrapper<T> diff --git a/src/validation.cpp b/src/validation.cpp index 6ed5e7a548..b532888f37 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -357,7 +357,7 @@ void CChainState::MaybeUpdateMempoolForReorg( AssertLockHeld(::cs_main); const CTransaction& tx = it->GetTx(); LockPoints lp = it->GetLockPoints(); - bool validLP = TestLockPointValidity(m_chain, &lp); + const bool validLP{TestLockPointValidity(m_chain, lp)}; CCoinsViewMemPool view_mempool(&CoinsTip(), *m_mempool); if (!CheckFinalTx(m_chain.Tip(), tx, flags) || !CheckSequenceLocks(m_chain.Tip(), view_mempool, tx, flags, &lp, validLP)) { @@ -371,8 +371,8 @@ void CChainState::MaybeUpdateMempoolForReorg( continue; const Coin &coin = CoinsTip().AccessCoin(txin.prevout); assert(!coin.IsSpent()); - unsigned int nMemPoolHeight = m_chain.Tip()->nHeight + 1; - if (coin.IsSpent() || (coin.IsCoinBase() && ((signed long)nMemPoolHeight) - coin.nHeight < COINBASE_MATURITY)) { + const auto mempool_spend_height{m_chain.Tip()->nHeight + 1}; + if (coin.IsSpent() || (coin.IsCoinBase() && mempool_spend_height - coin.nHeight < COINBASE_MATURITY)) { should_remove = true; break; } diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpc/backup.cpp index db22a19a63..c8242dff69 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1808,11 +1808,10 @@ RPCHelpMan listdescriptors() } spk.pushKV("desc", descriptor); spk.pushKV("timestamp", wallet_descriptor.creation_time); - const bool active = active_spk_mans.count(desc_spk_man) != 0; - spk.pushKV("active", active); - const auto& type = wallet_descriptor.descriptor->GetOutputType(); - if (active && type) { - spk.pushKV("internal", wallet->GetScriptPubKeyMan(*type, true) == desc_spk_man); + spk.pushKV("active", active_spk_mans.count(desc_spk_man) != 0); + const auto internal = wallet->IsInternalScriptPubKeyMan(desc_spk_man); + if (internal.has_value()) { + spk.pushKV("internal", *internal); } if (wallet_descriptor.descriptor->IsRange()) { UniValue range(UniValue::VARR); @@ -1832,3 +1831,99 @@ RPCHelpMan listdescriptors() }, }; } + +RPCHelpMan backupwallet() +{ + return RPCHelpMan{"backupwallet", + "\nSafely copies current wallet file to destination, which can be a directory or a path with filename.\n", + { + {"destination", RPCArg::Type::STR, RPCArg::Optional::NO, "The destination directory or file"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("backupwallet", "\"backup.dat\"") + + HelpExampleRpc("backupwallet", "\"backup.dat\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + std::string strDest = request.params[0].get_str(); + if (!pwallet->BackupWallet(strDest)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Error: Wallet backup failed!"); + } + + return NullUniValue; +}, + }; +} + + +RPCHelpMan restorewallet() +{ + return RPCHelpMan{ + "restorewallet", + "\nRestore and loads a wallet from backup.\n", + { + {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name that will be applied to the restored wallet"}, + {"backup_file", RPCArg::Type::STR, RPCArg::Optional::NO, "The backup file that will be used to restore the wallet."}, + {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED_NAMED_ARG, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "name", "The wallet name if restored successfully."}, + {RPCResult::Type::STR, "warning", "Warning message if wallet was not loaded cleanly."}, + } + }, + RPCExamples{ + HelpExampleCli("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"") + + HelpExampleRpc("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"") + + HelpExampleCliNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}}) + + HelpExampleRpcNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}}) + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + + WalletContext& context = EnsureWalletContext(request.context); + + auto backup_file = fs::u8path(request.params[1].get_str()); + + if (!fs::exists(backup_file)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Backup file does not exist"); + } + + std::string wallet_name = request.params[0].get_str(); + + const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), fs::u8path(wallet_name)); + + if (fs::exists(wallet_path)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Wallet name already exists."); + } + + if (!TryCreateDirectories(wallet_path)) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Failed to create database path '%s'. Database already exists.", wallet_path.u8string())); + } + + auto wallet_file = wallet_path / "wallet.dat"; + + fs::copy_file(backup_file, wallet_file, fs::copy_option::fail_if_exists); + + auto [wallet, warnings] = LoadWalletHelper(context, request.params[2], wallet_name); + + UniValue obj(UniValue::VOBJ); + obj.pushKV("name", wallet->GetName()); + obj.pushKV("warning", Join(warnings, Untranslated("\n")).original); + + return obj; + +}, + }; +} diff --git a/src/wallet/rpc/encrypt.cpp b/src/wallet/rpc/encrypt.cpp new file mode 100644 index 0000000000..e659f434a3 --- /dev/null +++ b/src/wallet/rpc/encrypt.cpp @@ -0,0 +1,248 @@ +// Copyright (c) 2011-2021 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include <rpc/util.h> +#include <wallet/rpc/util.h> +#include <wallet/wallet.h> + + +RPCHelpMan walletpassphrase() +{ + return RPCHelpMan{"walletpassphrase", + "\nStores the wallet decryption key in memory for 'timeout' seconds.\n" + "This is needed prior to performing transactions related to private keys such as sending bitcoins\n" + "\nNote:\n" + "Issuing the walletpassphrase command while the wallet is already unlocked will set a new unlock\n" + "time that overrides the old one.\n", + { + {"passphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The wallet passphrase"}, + {"timeout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The time to keep the decryption key in seconds; capped at 100000000 (~3 years)."}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + "\nUnlock the wallet for 60 seconds\n" + + HelpExampleCli("walletpassphrase", "\"my pass phrase\" 60") + + "\nLock the wallet again (before 60 seconds)\n" + + HelpExampleCli("walletlock", "") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("walletpassphrase", "\"my pass phrase\", 60") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + + int64_t nSleepTime; + int64_t relock_time; + // Prevent concurrent calls to walletpassphrase with the same wallet. + LOCK(pwallet->m_unlock_mutex); + { + LOCK(pwallet->cs_wallet); + + if (!pwallet->IsCrypted()) { + throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrase was called."); + } + + // Note that the walletpassphrase is stored in request.params[0] which is not mlock()ed + SecureString strWalletPass; + strWalletPass.reserve(100); + // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string) + // Alternately, find a way to make request.params[0] mlock()'d to begin with. + strWalletPass = request.params[0].get_str().c_str(); + + // Get the timeout + nSleepTime = request.params[1].get_int64(); + // Timeout cannot be negative, otherwise it will relock immediately + if (nSleepTime < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Timeout cannot be negative."); + } + // Clamp timeout + constexpr int64_t MAX_SLEEP_TIME = 100000000; // larger values trigger a macos/libevent bug? + if (nSleepTime > MAX_SLEEP_TIME) { + nSleepTime = MAX_SLEEP_TIME; + } + + if (strWalletPass.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); + } + + if (!pwallet->Unlock(strWalletPass)) { + throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect."); + } + + pwallet->TopUpKeyPool(); + + pwallet->nRelockTime = GetTime() + nSleepTime; + relock_time = pwallet->nRelockTime; + } + + // rpcRunLater must be called without cs_wallet held otherwise a deadlock + // can occur. The deadlock would happen when RPCRunLater removes the + // previous timer (and waits for the callback to finish if already running) + // and the callback locks cs_wallet. + AssertLockNotHeld(wallet->cs_wallet); + // Keep a weak pointer to the wallet so that it is possible to unload the + // wallet before the following callback is called. If a valid shared pointer + // is acquired in the callback then the wallet is still loaded. + std::weak_ptr<CWallet> weak_wallet = wallet; + pwallet->chain().rpcRunLater(strprintf("lockwallet(%s)", pwallet->GetName()), [weak_wallet, relock_time] { + if (auto shared_wallet = weak_wallet.lock()) { + LOCK(shared_wallet->cs_wallet); + // Skip if this is not the most recent rpcRunLater callback. + if (shared_wallet->nRelockTime != relock_time) return; + shared_wallet->Lock(); + shared_wallet->nRelockTime = 0; + } + }, nSleepTime); + + return NullUniValue; +}, + }; +} + + +RPCHelpMan walletpassphrasechange() +{ + return RPCHelpMan{"walletpassphrasechange", + "\nChanges the wallet passphrase from 'oldpassphrase' to 'newpassphrase'.\n", + { + {"oldpassphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The current passphrase"}, + {"newpassphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The new passphrase"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("walletpassphrasechange", "\"old one\" \"new one\"") + + HelpExampleRpc("walletpassphrasechange", "\"old one\", \"new one\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + if (!pwallet->IsCrypted()) { + throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrasechange was called."); + } + + // TODO: get rid of these .c_str() calls by implementing SecureString::operator=(std::string) + // Alternately, find a way to make request.params[0] mlock()'d to begin with. + SecureString strOldWalletPass; + strOldWalletPass.reserve(100); + strOldWalletPass = request.params[0].get_str().c_str(); + + SecureString strNewWalletPass; + strNewWalletPass.reserve(100); + strNewWalletPass = request.params[1].get_str().c_str(); + + if (strOldWalletPass.empty() || strNewWalletPass.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); + } + + if (!pwallet->ChangeWalletPassphrase(strOldWalletPass, strNewWalletPass)) { + throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect."); + } + + return NullUniValue; +}, + }; +} + + +RPCHelpMan walletlock() +{ + return RPCHelpMan{"walletlock", + "\nRemoves the wallet encryption key from memory, locking the wallet.\n" + "After calling this method, you will need to call walletpassphrase again\n" + "before being able to call any methods which require the wallet to be unlocked.\n", + {}, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + "\nSet the passphrase for 2 minutes to perform a transaction\n" + + HelpExampleCli("walletpassphrase", "\"my pass phrase\" 120") + + "\nPerform a send (requires passphrase set)\n" + + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 1.0") + + "\nClear the passphrase since we are done before 2 minutes is up\n" + + HelpExampleCli("walletlock", "") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("walletlock", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + if (!pwallet->IsCrypted()) { + throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletlock was called."); + } + + pwallet->Lock(); + pwallet->nRelockTime = 0; + + return NullUniValue; +}, + }; +} + + +RPCHelpMan encryptwallet() +{ + return RPCHelpMan{"encryptwallet", + "\nEncrypts the wallet with 'passphrase'. This is for first time encryption.\n" + "After this, any calls that interact with private keys such as sending or signing \n" + "will require the passphrase to be set prior the making these calls.\n" + "Use the walletpassphrase call for this, and then walletlock call.\n" + "If the wallet is already encrypted, use the walletpassphrasechange call.\n", + { + {"passphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The pass phrase to encrypt the wallet with. It must be at least 1 character, but should be long."}, + }, + RPCResult{RPCResult::Type::STR, "", "A string with further instructions"}, + RPCExamples{ + "\nEncrypt your wallet\n" + + HelpExampleCli("encryptwallet", "\"my pass phrase\"") + + "\nNow set the passphrase to use the wallet, such as for signing or sending bitcoin\n" + + HelpExampleCli("walletpassphrase", "\"my pass phrase\"") + + "\nNow we can do something like sign\n" + + HelpExampleCli("signmessage", "\"address\" \"test message\"") + + "\nNow lock the wallet again by removing the passphrase\n" + + HelpExampleCli("walletlock", "") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("encryptwallet", "\"my pass phrase\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return NullUniValue; + + LOCK(pwallet->cs_wallet); + + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: wallet does not contain private keys, nothing to encrypt."); + } + + if (pwallet->IsCrypted()) { + throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an encrypted wallet, but encryptwallet was called."); + } + + // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string) + // Alternately, find a way to make request.params[0] mlock()'d to begin with. + SecureString strWalletPass; + strWalletPass.reserve(100); + strWalletPass = request.params[0].get_str().c_str(); + + if (strWalletPass.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); + } + + if (!pwallet->EncryptWallet(strWalletPass)) { + throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: Failed to encrypt the wallet."); + } + + return "wallet encrypted; The keypool has been flushed and a new HD seed was generated (if you are using HD). You need to make a new backup."; +}, + }; +} diff --git a/src/wallet/rpc/util.cpp b/src/wallet/rpc/util.cpp index b926bfc75f..e2126b7236 100644 --- a/src/wallet/rpc/util.cpp +++ b/src/wallet/rpc/util.cpp @@ -5,6 +5,7 @@ #include <wallet/rpc/util.h> #include <rpc/util.h> +#include <util/translation.h> #include <util/url.h> #include <wallet/context.h> #include <wallet/wallet.h> @@ -120,3 +121,34 @@ std::string LabelFromValue(const UniValue& value) throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME, "Invalid label name"); return label; } + +std::tuple<std::shared_ptr<CWallet>, std::vector<bilingual_str>> LoadWalletHelper(WalletContext& context, UniValue load_on_start_param, const std::string wallet_name) +{ + DatabaseOptions options; + DatabaseStatus status; + options.require_existing = true; + bilingual_str error; + std::vector<bilingual_str> warnings; + std::optional<bool> load_on_start = load_on_start_param.isNull() ? std::nullopt : std::optional<bool>(load_on_start_param.get_bool()); + std::shared_ptr<CWallet> const wallet = LoadWallet(context, wallet_name, load_on_start, options, status, error, warnings); + + if (!wallet) { + // Map bad format to not found, since bad format is returned when the + // wallet directory exists, but doesn't contain a data file. + RPCErrorCode code = RPC_WALLET_ERROR; + switch (status) { + case DatabaseStatus::FAILED_NOT_FOUND: + case DatabaseStatus::FAILED_BAD_FORMAT: + code = RPC_WALLET_NOT_FOUND; + break; + case DatabaseStatus::FAILED_ALREADY_LOADED: + code = RPC_WALLET_ALREADY_LOADED; + break; + default: // RPC_WALLET_ERROR is returned for all other cases. + break; + } + throw JSONRPCError(code, error.original); + } + + return { wallet, warnings }; +} diff --git a/src/wallet/rpc/util.h b/src/wallet/rpc/util.h index a493a80a74..a1fa4d49b1 100644 --- a/src/wallet/rpc/util.h +++ b/src/wallet/rpc/util.h @@ -8,7 +8,9 @@ #include <any> #include <memory> #include <string> +#include <vector> +struct bilingual_str; class CWallet; class JSONRPCRequest; class LegacyScriptPubKeyMan; @@ -35,4 +37,6 @@ bool GetAvoidReuseFlag(const CWallet& wallet, const UniValue& param); bool ParseIncludeWatchonly(const UniValue& include_watchonly, const CWallet& wallet); std::string LabelFromValue(const UniValue& value); +std::tuple<std::shared_ptr<CWallet>, std::vector<bilingual_str>> LoadWalletHelper(WalletContext& context, UniValue load_on_start_param, const std::string wallet_name); + #endif // BITCOIN_WALLET_RPC_UTIL_H diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 8cb2c46b63..099969a609 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1656,41 +1656,6 @@ static RPCHelpMan abandontransaction() }; } - -static RPCHelpMan backupwallet() -{ - return RPCHelpMan{"backupwallet", - "\nSafely copies current wallet file to destination, which can be a directory or a path with filename.\n", - { - {"destination", RPCArg::Type::STR, RPCArg::Optional::NO, "The destination directory or file"}, - }, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{ - HelpExampleCli("backupwallet", "\"backup.dat\"") - + HelpExampleRpc("backupwallet", "\"backup.dat\"") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - pwallet->BlockUntilSyncedToCurrentChain(); - - LOCK(pwallet->cs_wallet); - - std::string strDest = request.params[0].get_str(); - if (!pwallet->BackupWallet(strDest)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Error: Wallet backup failed!"); - } - - return NullUniValue; -}, - }; -} - - static RPCHelpMan keypoolrefill() { return RPCHelpMan{"keypoolrefill", @@ -1762,247 +1727,6 @@ static RPCHelpMan newkeypool() }; } - -static RPCHelpMan walletpassphrase() -{ - return RPCHelpMan{"walletpassphrase", - "\nStores the wallet decryption key in memory for 'timeout' seconds.\n" - "This is needed prior to performing transactions related to private keys such as sending bitcoins\n" - "\nNote:\n" - "Issuing the walletpassphrase command while the wallet is already unlocked will set a new unlock\n" - "time that overrides the old one.\n", - { - {"passphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The wallet passphrase"}, - {"timeout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The time to keep the decryption key in seconds; capped at 100000000 (~3 years)."}, - }, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{ - "\nUnlock the wallet for 60 seconds\n" - + HelpExampleCli("walletpassphrase", "\"my pass phrase\" 60") + - "\nLock the wallet again (before 60 seconds)\n" - + HelpExampleCli("walletlock", "") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("walletpassphrase", "\"my pass phrase\", 60") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request); - if (!wallet) return NullUniValue; - CWallet* const pwallet = wallet.get(); - - int64_t nSleepTime; - int64_t relock_time; - // Prevent concurrent calls to walletpassphrase with the same wallet. - LOCK(pwallet->m_unlock_mutex); - { - LOCK(pwallet->cs_wallet); - - if (!pwallet->IsCrypted()) { - throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrase was called."); - } - - // Note that the walletpassphrase is stored in request.params[0] which is not mlock()ed - SecureString strWalletPass; - strWalletPass.reserve(100); - // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string) - // Alternately, find a way to make request.params[0] mlock()'d to begin with. - strWalletPass = request.params[0].get_str().c_str(); - - // Get the timeout - nSleepTime = request.params[1].get_int64(); - // Timeout cannot be negative, otherwise it will relock immediately - if (nSleepTime < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Timeout cannot be negative."); - } - // Clamp timeout - constexpr int64_t MAX_SLEEP_TIME = 100000000; // larger values trigger a macos/libevent bug? - if (nSleepTime > MAX_SLEEP_TIME) { - nSleepTime = MAX_SLEEP_TIME; - } - - if (strWalletPass.empty()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); - } - - if (!pwallet->Unlock(strWalletPass)) { - throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect."); - } - - pwallet->TopUpKeyPool(); - - pwallet->nRelockTime = GetTime() + nSleepTime; - relock_time = pwallet->nRelockTime; - } - - // rpcRunLater must be called without cs_wallet held otherwise a deadlock - // can occur. The deadlock would happen when RPCRunLater removes the - // previous timer (and waits for the callback to finish if already running) - // and the callback locks cs_wallet. - AssertLockNotHeld(wallet->cs_wallet); - // Keep a weak pointer to the wallet so that it is possible to unload the - // wallet before the following callback is called. If a valid shared pointer - // is acquired in the callback then the wallet is still loaded. - std::weak_ptr<CWallet> weak_wallet = wallet; - pwallet->chain().rpcRunLater(strprintf("lockwallet(%s)", pwallet->GetName()), [weak_wallet, relock_time] { - if (auto shared_wallet = weak_wallet.lock()) { - LOCK(shared_wallet->cs_wallet); - // Skip if this is not the most recent rpcRunLater callback. - if (shared_wallet->nRelockTime != relock_time) return; - shared_wallet->Lock(); - shared_wallet->nRelockTime = 0; - } - }, nSleepTime); - - return NullUniValue; -}, - }; -} - - -static RPCHelpMan walletpassphrasechange() -{ - return RPCHelpMan{"walletpassphrasechange", - "\nChanges the wallet passphrase from 'oldpassphrase' to 'newpassphrase'.\n", - { - {"oldpassphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The current passphrase"}, - {"newpassphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The new passphrase"}, - }, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{ - HelpExampleCli("walletpassphrasechange", "\"old one\" \"new one\"") - + HelpExampleRpc("walletpassphrasechange", "\"old one\", \"new one\"") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - LOCK(pwallet->cs_wallet); - - if (!pwallet->IsCrypted()) { - throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrasechange was called."); - } - - // TODO: get rid of these .c_str() calls by implementing SecureString::operator=(std::string) - // Alternately, find a way to make request.params[0] mlock()'d to begin with. - SecureString strOldWalletPass; - strOldWalletPass.reserve(100); - strOldWalletPass = request.params[0].get_str().c_str(); - - SecureString strNewWalletPass; - strNewWalletPass.reserve(100); - strNewWalletPass = request.params[1].get_str().c_str(); - - if (strOldWalletPass.empty() || strNewWalletPass.empty()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); - } - - if (!pwallet->ChangeWalletPassphrase(strOldWalletPass, strNewWalletPass)) { - throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect."); - } - - return NullUniValue; -}, - }; -} - - -static RPCHelpMan walletlock() -{ - return RPCHelpMan{"walletlock", - "\nRemoves the wallet encryption key from memory, locking the wallet.\n" - "After calling this method, you will need to call walletpassphrase again\n" - "before being able to call any methods which require the wallet to be unlocked.\n", - {}, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{ - "\nSet the passphrase for 2 minutes to perform a transaction\n" - + HelpExampleCli("walletpassphrase", "\"my pass phrase\" 120") + - "\nPerform a send (requires passphrase set)\n" - + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 1.0") + - "\nClear the passphrase since we are done before 2 minutes is up\n" - + HelpExampleCli("walletlock", "") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("walletlock", "") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - LOCK(pwallet->cs_wallet); - - if (!pwallet->IsCrypted()) { - throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletlock was called."); - } - - pwallet->Lock(); - pwallet->nRelockTime = 0; - - return NullUniValue; -}, - }; -} - - -static RPCHelpMan encryptwallet() -{ - return RPCHelpMan{"encryptwallet", - "\nEncrypts the wallet with 'passphrase'. This is for first time encryption.\n" - "After this, any calls that interact with private keys such as sending or signing \n" - "will require the passphrase to be set prior the making these calls.\n" - "Use the walletpassphrase call for this, and then walletlock call.\n" - "If the wallet is already encrypted, use the walletpassphrasechange call.\n", - { - {"passphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The pass phrase to encrypt the wallet with. It must be at least 1 character, but should be long."}, - }, - RPCResult{RPCResult::Type::STR, "", "A string with further instructions"}, - RPCExamples{ - "\nEncrypt your wallet\n" - + HelpExampleCli("encryptwallet", "\"my pass phrase\"") + - "\nNow set the passphrase to use the wallet, such as for signing or sending bitcoin\n" - + HelpExampleCli("walletpassphrase", "\"my pass phrase\"") + - "\nNow we can do something like sign\n" - + HelpExampleCli("signmessage", "\"address\" \"test message\"") + - "\nNow lock the wallet again by removing the passphrase\n" - + HelpExampleCli("walletlock", "") + - "\nAs a JSON-RPC call\n" - + HelpExampleRpc("encryptwallet", "\"my pass phrase\"") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return NullUniValue; - - LOCK(pwallet->cs_wallet); - - if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { - throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: wallet does not contain private keys, nothing to encrypt."); - } - - if (pwallet->IsCrypted()) { - throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an encrypted wallet, but encryptwallet was called."); - } - - // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string) - // Alternately, find a way to make request.params[0] mlock()'d to begin with. - SecureString strWalletPass; - strWalletPass.reserve(100); - strWalletPass = request.params[0].get_str().c_str(); - - if (strWalletPass.empty()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); - } - - if (!pwallet->EncryptWallet(strWalletPass)) { - throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: Failed to encrypt the wallet."); - } - - return "wallet encrypted; The keypool has been flushed and a new HD seed was generated (if you are using HD). You need to make a new backup."; -}, - }; -} - static RPCHelpMan lockunspent() { return RPCHelpMan{"lockunspent", @@ -2467,37 +2191,6 @@ static RPCHelpMan listwallets() }; } -static std::tuple<std::shared_ptr<CWallet>, std::vector<bilingual_str>> LoadWalletHelper(WalletContext& context, UniValue load_on_start_param, const std::string wallet_name) -{ - DatabaseOptions options; - DatabaseStatus status; - options.require_existing = true; - bilingual_str error; - std::vector<bilingual_str> warnings; - std::optional<bool> load_on_start = load_on_start_param.isNull() ? std::nullopt : std::optional<bool>(load_on_start_param.get_bool()); - std::shared_ptr<CWallet> const wallet = LoadWallet(context, wallet_name, load_on_start, options, status, error, warnings); - - if (!wallet) { - // Map bad format to not found, since bad format is returned when the - // wallet directory exists, but doesn't contain a data file. - RPCErrorCode code = RPC_WALLET_ERROR; - switch (status) { - case DatabaseStatus::FAILED_NOT_FOUND: - case DatabaseStatus::FAILED_BAD_FORMAT: - code = RPC_WALLET_NOT_FOUND; - break; - case DatabaseStatus::FAILED_ALREADY_LOADED: - code = RPC_WALLET_ALREADY_LOADED; - break; - default: // RPC_WALLET_ERROR is returned for all other cases. - break; - } - throw JSONRPCError(code, error.original); - } - - return { wallet, warnings }; -} - static RPCHelpMan loadwallet() { return RPCHelpMan{"loadwallet", @@ -2697,68 +2390,6 @@ static RPCHelpMan createwallet() }; } -static RPCHelpMan restorewallet() -{ - return RPCHelpMan{ - "restorewallet", - "\nRestore and loads a wallet from backup.\n", - { - {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name that will be applied to the restored wallet"}, - {"backup_file", RPCArg::Type::STR, RPCArg::Optional::NO, "The backup file that will be used to restore the wallet."}, - {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED_NAMED_ARG, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, - }, - RPCResult{ - RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::STR, "name", "The wallet name if restored successfully."}, - {RPCResult::Type::STR, "warning", "Warning message if wallet was not loaded cleanly."}, - } - }, - RPCExamples{ - HelpExampleCli("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"") - + HelpExampleRpc("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"") - + HelpExampleCliNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}}) - + HelpExampleRpcNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}}) - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - - WalletContext& context = EnsureWalletContext(request.context); - - auto backup_file = fs::u8path(request.params[1].get_str()); - - if (!fs::exists(backup_file)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Backup file does not exist"); - } - - std::string wallet_name = request.params[0].get_str(); - - const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), fs::u8path(wallet_name)); - - if (fs::exists(wallet_path)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Wallet name already exists."); - } - - if (!TryCreateDirectories(wallet_path)) { - throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Failed to create database path '%s'. Database already exists.", wallet_path.u8string())); - } - - auto wallet_file = wallet_path / "wallet.dat"; - - fs::copy_file(backup_file, wallet_file, fs::copy_option::fail_if_exists); - - auto [wallet, warnings] = LoadWalletHelper(context, request.params[2], wallet_name); - - UniValue obj(UniValue::VOBJ); - obj.pushKV("name", wallet->GetName()); - obj.pushKV("warning", Join(warnings, Untranslated("\n")).original); - - return obj; - -}, - }; -} - static RPCHelpMan unloadwallet() { return RPCHelpMan{"unloadwallet", @@ -3940,8 +3571,12 @@ RPCHelpMan getaddressinfo() ret.pushKV("solvable", false); } + const auto& spk_mans = pwallet->GetScriptPubKeyMans(scriptPubKey); + // In most cases there is only one matching ScriptPubKey manager and we can't resolve ambiguity in a better way + ScriptPubKeyMan* spk_man{nullptr}; + if (spk_mans.size()) spk_man = *spk_mans.begin(); - DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(pwallet->GetScriptPubKeyMan(scriptPubKey)); + DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man); if (desc_spk_man) { std::string desc_str; if (desc_spk_man->GetDescriptorString(desc_str, /* priv */ false)) { @@ -3956,7 +3591,6 @@ RPCHelpMan getaddressinfo() ret.pushKV("ischange", ScriptIsChange(*pwallet, scriptPubKey)); - ScriptPubKeyMan* spk_man = pwallet->GetScriptPubKeyMan(scriptPubKey); if (spk_man) { if (const std::unique_ptr<CKeyMetadata> meta = spk_man->GetMetadata(dest)) { ret.pushKV("timestamp", meta->nCreateTime); @@ -4696,6 +4330,14 @@ RPCHelpMan importmulti(); RPCHelpMan importdescriptors(); RPCHelpMan listdescriptors(); RPCHelpMan signmessage(); +RPCHelpMan backupwallet(); +RPCHelpMan restorewallet(); + +// encryption +RPCHelpMan walletpassphrase(); +RPCHelpMan walletpassphrasechange(); +RPCHelpMan walletlock(); +RPCHelpMan encryptwallet(); Span<const CRPCCommand> GetWalletRPCCommands() { diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index a82eaa4879..1769429efe 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -354,15 +354,22 @@ bool LegacyScriptPubKeyMan::TopUpInactiveHDChain(const CKeyID seed_id, int64_t i return true; } -void LegacyScriptPubKeyMan::MarkUnusedAddresses(const CScript& script) +std::vector<WalletDestination> LegacyScriptPubKeyMan::MarkUnusedAddresses(const CScript& script) { LOCK(cs_KeyStore); + std::vector<WalletDestination> result; // extract addresses and check if they match with an unused keypool key for (const auto& keyid : GetAffectedKeys(script, *this)) { std::map<CKeyID, int64_t>::const_iterator mi = m_pool_key_to_index.find(keyid); if (mi != m_pool_key_to_index.end()) { WalletLogPrintf("%s: Detected a used keypool key, mark all keypool keys up to this key as used\n", __func__); - MarkReserveKeysAsUsed(mi->second); + for (const auto& keypool : MarkReserveKeysAsUsed(mi->second)) { + // derive all possible destinations as any of them could have been used + for (const auto& type : LEGACY_OUTPUT_TYPES) { + const auto& dest = GetDestinationForKey(keypool.vchPubKey, type); + result.push_back({dest, keypool.fInternal}); + } + } if (!TopUp()) { WalletLogPrintf("%s: Topping up keypool failed (locked wallet)\n", __func__); @@ -384,6 +391,8 @@ void LegacyScriptPubKeyMan::MarkUnusedAddresses(const CScript& script) } } } + + return result; } void LegacyScriptPubKeyMan::UpgradeKeyMetadata() @@ -1427,7 +1436,7 @@ void LegacyScriptPubKeyMan::LearnAllRelatedScripts(const CPubKey& key) LearnRelatedScripts(key, OutputType::P2SH_SEGWIT); } -void LegacyScriptPubKeyMan::MarkReserveKeysAsUsed(int64_t keypool_id) +std::vector<CKeyPool> LegacyScriptPubKeyMan::MarkReserveKeysAsUsed(int64_t keypool_id) { AssertLockHeld(cs_KeyStore); bool internal = setInternalKeyPool.count(keypool_id); @@ -1435,6 +1444,7 @@ void LegacyScriptPubKeyMan::MarkReserveKeysAsUsed(int64_t keypool_id) std::set<int64_t> *setKeyPool = internal ? &setInternalKeyPool : (set_pre_split_keypool.empty() ? &setExternalKeyPool : &set_pre_split_keypool); auto it = setKeyPool->begin(); + std::vector<CKeyPool> result; WalletBatch batch(m_storage.GetDatabase()); while (it != std::end(*setKeyPool)) { const int64_t& index = *(it); @@ -1448,7 +1458,10 @@ void LegacyScriptPubKeyMan::MarkReserveKeysAsUsed(int64_t keypool_id) batch.ErasePool(index); WalletLogPrintf("keypool index %d removed\n", index); it = setKeyPool->erase(it); + result.push_back(std::move(keypool)); } + + return result; } std::vector<CKeyID> GetAffectedKeys(const CScript& spk, const SigningProvider& provider) @@ -1820,19 +1833,32 @@ bool DescriptorScriptPubKeyMan::TopUp(unsigned int size) return true; } -void DescriptorScriptPubKeyMan::MarkUnusedAddresses(const CScript& script) +std::vector<WalletDestination> DescriptorScriptPubKeyMan::MarkUnusedAddresses(const CScript& script) { LOCK(cs_desc_man); + std::vector<WalletDestination> result; if (IsMine(script)) { int32_t index = m_map_script_pub_keys[script]; if (index >= m_wallet_descriptor.next_index) { WalletLogPrintf("%s: Detected a used keypool item at index %d, mark all keypool items up to this item as used\n", __func__, index); - m_wallet_descriptor.next_index = index + 1; + auto out_keys = std::make_unique<FlatSigningProvider>(); + std::vector<CScript> scripts_temp; + while (index >= m_wallet_descriptor.next_index) { + if (!m_wallet_descriptor.descriptor->ExpandFromCache(m_wallet_descriptor.next_index, m_wallet_descriptor.cache, scripts_temp, *out_keys)) { + throw std::runtime_error(std::string(__func__) + ": Unable to expand descriptor from cache"); + } + CTxDestination dest; + ExtractDestination(scripts_temp[0], dest); + result.push_back({dest, std::nullopt}); + m_wallet_descriptor.next_index++; + } } if (!TopUp()) { WalletLogPrintf("%s: Topping up keypool failed (locked wallet)\n", __func__); } } + + return result; } void DescriptorScriptPubKeyMan::AddDescriptorKey(const CKey& key, const CPubKey &pubkey) diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 9d2304a542..ebe064fa0a 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -149,6 +149,12 @@ public: } }; +struct WalletDestination +{ + CTxDestination dest; + std::optional<bool> internal; +}; + /* * A class implementing ScriptPubKeyMan manages some (or all) scriptPubKeys used in a wallet. * It contains the scripts and keys related to the scriptPubKeys it manages. @@ -181,8 +187,14 @@ public: */ virtual bool TopUp(unsigned int size = 0) { return false; } - //! Mark unused addresses as being used - virtual void MarkUnusedAddresses(const CScript& script) {} + /** Mark unused addresses as being used + * Affects all keys up to and including the one determined by provided script. + * + * @param script determines the last key to mark as used + * + * @return All of the addresses affected + */ + virtual std::vector<WalletDestination> MarkUnusedAddresses(const CScript& script) { return {}; } /** Sets up the key generation stuff, i.e. generates new HD seeds and sets them as active. * Returns false if already setup or setup fails, true if setup is successful @@ -357,7 +369,7 @@ public: bool TopUp(unsigned int size = 0) override; - void MarkUnusedAddresses(const CScript& script) override; + std::vector<WalletDestination> MarkUnusedAddresses(const CScript& script) override; //! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo void UpgradeKeyMetadata(); @@ -482,9 +494,13 @@ public: void LearnAllRelatedScripts(const CPubKey& key); /** - * Marks all keys in the keypool up to and including reserve_key as used. + * Marks all keys in the keypool up to and including the provided key as used. + * + * @param keypool_id determines the last key to mark as used + * + * @return All affected keys */ - void MarkReserveKeysAsUsed(int64_t keypool_id) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); + std::vector<CKeyPool> MarkReserveKeysAsUsed(int64_t keypool_id) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); const std::map<CKeyID, int64_t>& GetAllReserveKeys() const { return m_pool_key_to_index; } std::set<CKeyID> GetKeys() const override; @@ -564,7 +580,7 @@ public: // (with or without private keys), the "keypool" is a single xpub. bool TopUp(unsigned int size = 0) override; - void MarkUnusedAddresses(const CScript& script) override; + std::vector<WalletDestination> MarkUnusedAddresses(const CScript& script) override; bool IsHDEnabled() const override; diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 6925d9ef2a..b4141d1f9e 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -816,30 +816,35 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) context.args = &gArgs; context.chain = m_node.chain.get(); auto wallet = TestLoadWallet(context); - CKey key; - key.MakeNewKey(true); - AddKey(*wallet, key); + AddKey(*wallet, coinbaseKey); - std::string error; + // rescan to ensure coinbase transactions from test fixture are picked up by the wallet + { + WalletRescanReserver reserver(*wallet); + reserver.reserve(); + wallet->ScanForWalletTransactions(m_node.chain->getBlockHash(0), 0, /* max height= */ {}, reserver, /* update= */ true); + } + // create one more block to get the first block coinbase to maturity m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); - auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); - CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + // spend first coinbase tx + auto spend_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + CreateAndProcessBlock({spend_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); SyncWithValidationInterfaceQueue(); { - auto block_hash = block_tx.GetHash(); + auto spend_tx_hash = spend_tx.GetHash(); auto prev_hash = m_coinbase_txns[0]->GetHash(); LOCK(wallet->cs_wallet); BOOST_CHECK(wallet->HasWalletSpend(prev_hash)); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(spend_tx_hash), 1u); - std::vector<uint256> vHashIn{ block_hash }, vHashOut; + std::vector<uint256> vHashIn{spend_tx_hash}, vHashOut; BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK); BOOST_CHECK(!wallet->HasWalletSpend(prev_hash)); - BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u); + BOOST_CHECK_EQUAL(wallet->mapWallet.count(spend_tx_hash), 0u); } TestUnloadWallet(std::move(wallet)); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e4c3822305..2e4b6a6eb0 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -919,7 +919,9 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const wtx.nOrderPos = IncOrderPosNext(&batch); wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx)); wtx.nTimeSmart = ComputeTimeSmart(wtx, rescanning_old_block); - AddToSpends(hash, &batch); + if (IsFromMe(*tx.get())) { + AddToSpends(hash); + } } if (!fInsertedNew) @@ -1061,8 +1063,23 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const SyncTxS // loop though all outputs for (const CTxOut& txout: tx.vout) { - for (const auto& spk_man_pair : m_spk_managers) { - spk_man_pair.second->MarkUnusedAddresses(txout.scriptPubKey); + for (const auto& spk_man : GetScriptPubKeyMans(txout.scriptPubKey)) { + for (auto &dest : spk_man->MarkUnusedAddresses(txout.scriptPubKey)) { + // If internal flag is not defined try to infer it from the ScriptPubKeyMan + if (!dest.internal.has_value()) { + dest.internal = IsInternalScriptPubKeyMan(spk_man); + } + + // skip if can't determine whether it's a receiving address or not + if (!dest.internal.has_value()) continue; + + // If this is a receiving address and it's not in the address book yet + // (e.g. it wasn't generated on this node or we're restoring from backup) + // add it to the address book for proper transaction accounting + if (!*dest.internal && !FindAddressBookEntry(dest.dest, /* allow_change= */ false)) { + SetAddressBook(dest.dest, "", "receive"); + } + } } } @@ -2253,16 +2270,15 @@ void ReserveDestination::ReturnDestination() bool CWallet::DisplayAddress(const CTxDestination& dest) { CScript scriptPubKey = GetScriptForDestination(dest); - const auto spk_man = GetScriptPubKeyMan(scriptPubKey); - if (spk_man == nullptr) { - return false; - } - auto signer_spk_man = dynamic_cast<ExternalSignerScriptPubKeyMan*>(spk_man); - if (signer_spk_man == nullptr) { - return false; + for (const auto& spk_man : GetScriptPubKeyMans(scriptPubKey)) { + auto signer_spk_man = dynamic_cast<ExternalSignerScriptPubKeyMan *>(spk_man); + if (signer_spk_man == nullptr) { + continue; + } + ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner(); + return signer_spk_man->DisplayAddress(scriptPubKey, signer); } - ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner(); - return signer_spk_man->DisplayAddress(scriptPubKey, signer); + return false; } bool CWallet::LockCoin(const COutPoint& output, WalletBatch* batch) @@ -3050,9 +3066,10 @@ ScriptPubKeyMan* CWallet::GetScriptPubKeyMan(const OutputType& type, bool intern return it->second; } -std::set<ScriptPubKeyMan*> CWallet::GetScriptPubKeyMans(const CScript& script, SignatureData& sigdata) const +std::set<ScriptPubKeyMan*> CWallet::GetScriptPubKeyMans(const CScript& script) const { std::set<ScriptPubKeyMan*> spk_mans; + SignatureData sigdata; for (const auto& spk_man_pair : m_spk_managers) { if (spk_man_pair.second->CanProvide(script, sigdata)) { spk_mans.insert(spk_man_pair.second.get()); @@ -3061,17 +3078,6 @@ std::set<ScriptPubKeyMan*> CWallet::GetScriptPubKeyMans(const CScript& script, S return spk_mans; } -ScriptPubKeyMan* CWallet::GetScriptPubKeyMan(const CScript& script) const -{ - SignatureData sigdata; - for (const auto& spk_man_pair : m_spk_managers) { - if (spk_man_pair.second->CanProvide(script, sigdata)) { - return spk_man_pair.second.get(); - } - } - return nullptr; -} - ScriptPubKeyMan* CWallet::GetScriptPubKeyMan(const uint256& id) const { if (m_spk_managers.count(id) > 0) { @@ -3287,6 +3293,30 @@ DescriptorScriptPubKeyMan* CWallet::GetDescriptorScriptPubKeyMan(const WalletDes return nullptr; } +std::optional<bool> CWallet::IsInternalScriptPubKeyMan(ScriptPubKeyMan* spk_man) const +{ + // Legacy script pubkey man can't be either external or internal + if (IsLegacy()) { + return std::nullopt; + } + + // only active ScriptPubKeyMan can be internal + if (!GetActiveScriptPubKeyMans().count(spk_man)) { + return std::nullopt; + } + + const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man); + if (!desc_spk_man) { + throw std::runtime_error(std::string(__func__) + ": unexpected ScriptPubKeyMan type."); + } + + LOCK(desc_spk_man->cs_desc_man); + const auto& type = desc_spk_man->GetWalletDescriptor().descriptor->GetOutputType(); + assert(type.has_value()); + + return GetScriptPubKeyMan(*type, /* internal= */ true) == desc_spk_man; +} + ScriptPubKeyMan* CWallet::AddWalletDescriptor(WalletDescriptor& desc, const FlatSigningProvider& signing_provider, const std::string& label, bool internal) { AssertLockHeld(cs_wallet); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index e294358609..dbf0f6375d 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -809,14 +809,11 @@ public: //! Get the ScriptPubKeyMan for the given OutputType and internal/external chain. ScriptPubKeyMan* GetScriptPubKeyMan(const OutputType& type, bool internal) const; - //! Get the ScriptPubKeyMan for a script - ScriptPubKeyMan* GetScriptPubKeyMan(const CScript& script) const; + //! Get all the ScriptPubKeyMans for a script + std::set<ScriptPubKeyMan*> GetScriptPubKeyMans(const CScript& script) const; //! Get the ScriptPubKeyMan by id ScriptPubKeyMan* GetScriptPubKeyMan(const uint256& id) const; - //! Get all of the ScriptPubKeyMans for a script given additional information in sigdata (populated by e.g. a psbt) - std::set<ScriptPubKeyMan*> GetScriptPubKeyMans(const CScript& script, SignatureData& sigdata) const; - //! Get the SigningProvider for a script std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const; std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script, SignatureData& sigdata) const; @@ -882,6 +879,11 @@ public: //! Return the DescriptorScriptPubKeyMan for a WalletDescriptor if it is already in the wallet DescriptorScriptPubKeyMan* GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const; + //! Returns whether the provided ScriptPubKeyMan is internal + //! @param[in] spk_man The ScriptPubKeyMan to test + //! @return contains value only for active DescriptorScriptPubKeyMan, otherwise undefined + std::optional<bool> IsInternalScriptPubKeyMan(ScriptPubKeyMan* spk_man) const; + //! Add a descriptor to the wallet, return a ScriptPubKeyMan & associated output type ScriptPubKeyMan* AddWalletDescriptor(WalletDescriptor& desc, const FlatSigningProvider& signing_provider, const std::string& label, bool internal) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); }; diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 8fd15a164c..fc263f17ce 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -3,6 +3,10 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listtransactions API.""" + +import shutil +import os + from decimal import Decimal from test_framework.messages import ( @@ -17,7 +21,7 @@ from test_framework.util import ( class ListTransactionsTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 2 + self.num_nodes = 3 # This test isn't testing txn relay/timing, so set whitelist on the # peers for instant txn relay. This speeds up the test run time 2-3x. self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes @@ -102,7 +106,7 @@ class ListTransactionsTest(BitcoinTestFramework): {"txid": txid, "label": "watchonly"}) self.run_rbf_opt_in_test() - + self.run_externally_generated_address_test() def run_rbf_opt_in_test(self): """Test the opt-in-rbf flag for sent and received transactions.""" @@ -217,5 +221,63 @@ class ListTransactionsTest(BitcoinTestFramework): assert_equal(self.nodes[0].gettransaction(txid_3b)["bip125-replaceable"], "no") assert_equal(self.nodes[0].gettransaction(txid_4)["bip125-replaceable"], "unknown") + def run_externally_generated_address_test(self): + """Test behavior when receiving address is not in the address book.""" + + self.log.info("Setup the same wallet on two nodes") + # refill keypool otherwise the second node wouldn't recognize addresses generated on the first nodes + self.nodes[0].keypoolrefill(1000) + self.stop_nodes() + wallet0 = os.path.join(self.nodes[0].datadir, self.chain, self.default_wallet_name, "wallet.dat") + wallet2 = os.path.join(self.nodes[2].datadir, self.chain, self.default_wallet_name, "wallet.dat") + shutil.copyfile(wallet0, wallet2) + self.start_nodes() + # reconnect nodes + self.connect_nodes(0, 1) + self.connect_nodes(1, 2) + self.connect_nodes(2, 0) + + addr1 = self.nodes[0].getnewaddress("pizza1", 'legacy') + addr2 = self.nodes[0].getnewaddress("pizza2", 'p2sh-segwit') + addr3 = self.nodes[0].getnewaddress("pizza3", 'bech32') + + self.log.info("Send to externally generated addresses") + # send to an address beyond the next to be generated to test the keypool gap + self.nodes[1].sendtoaddress(addr3, "0.001") + self.generate(self.nodes[1], 1) + self.sync_all() + + # send to an address that is already marked as used due to the keypool gap mechanics + self.nodes[1].sendtoaddress(addr2, "0.001") + self.generate(self.nodes[1], 1) + self.sync_all() + + # send to self transaction + self.nodes[0].sendtoaddress(addr1, "0.001") + self.generate(self.nodes[0], 1) + self.sync_all() + + self.log.info("Verify listtransactions is the same regardless of where the address was generated") + transactions0 = self.nodes[0].listtransactions() + transactions2 = self.nodes[2].listtransactions() + + # normalize results: remove fields that normally could differ and sort + def normalize_list(txs): + for tx in txs: + tx.pop('label', None) + tx.pop('time', None) + tx.pop('timereceived', None) + txs.sort(key=lambda x: x['txid']) + + normalize_list(transactions0) + normalize_list(transactions2) + assert_equal(transactions0, transactions2) + + self.log.info("Verify labels are persistent on the node generated the addresses") + assert_equal(['pizza1'], self.nodes[0].getaddressinfo(addr1)['labels']) + assert_equal(['pizza2'], self.nodes[0].getaddressinfo(addr2)['labels']) + assert_equal(['pizza3'], self.nodes[0].getaddressinfo(addr3)['labels']) + + if __name__ == '__main__': ListTransactionsTest().main() |