diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 7f08ba98b510..e66ddd18eb88 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -113,12 +113,42 @@ class Wallet //! Initiate a rescan. Returns status indicating success, failure, abort, or already rescanning. virtual wallet::RescanStatus startRescan(bool from_genesis) = 0; + //! Rescan, on the calling thread, the blocks a key born at from_time + //! (UNIX time in seconds) may be paid in, as importdescriptors does. + //! Returns as startRescan() does. Only a completed rescanFromTime() or + //! startRescan() settles owed rescans: those it covers, except those + //! recorded after it started and those of an import that failed (see + //! ensureFriendshipReceivingKeychain()). Other rescans, such as + //! rescanblockchain's or one at wallet load, settle none. After a write + //! to the wallet database has failed since the wallet loaded, what a + //! rescan finds may not be saved, so the rescans it settles are owed + //! again once the wallet is reloaded. + //! FAILURE settles nothing and has two causes: a block it had to read has + //! no data (rescanUnavailableFrom() names the first block without data, + //! and the blocks this node has were scanned), or the chain changed + //! during the scan (rescanUnavailableFrom() is nullopt; run it again). + //! With a block filter index, a block whose filter does not match the + //! wallet is not read, so a rescan can complete and settle even while + //! rescanUnavailableFrom() names a height. + virtual wallet::RescanStatus rescanFromTime(int64_t from_time) = 0; + //! Abort a rescan. virtual void abortRescan() = 0; - //! The height startRescan(false) starts at, when this node has pruned - //! blocks it would read; nullopt when it has them all. - virtual std::optional rescanPrunedFrom() = 0; + //! The first height, up to the chain's tip, of the blocks + //! rescanFromTime(from_time) scans that this node has no block data + //! for, as on a pruned node or below an assumeutxo snapshot; nullopt + //! when it has them all. Such a rescan reads the blocks it has and + //! fails, unless block filters show none of the others pays the wallet. + virtual std::optional rescanUnavailableFrom(int64_t from_time) = 0; + + //! The time, in seconds, the rescan this wallet owes starts from, for + //! receiving chains it was given after it had scanned past their birth; + //! nullopt when it owes none. Run it with rescanFromTime(). It stays + //! owed while that rescan fails for want of the blocks + //! rescanUnavailableFrom() names. The rescan an import that failed owes + //! is left out until the chain is imported again or the wallet reloaded. + virtual std::optional owedRescanFrom() = 0; //! Lock masternode collaterals virtual void autoLockMasternodeCollaterals() = 0; @@ -171,12 +201,32 @@ class Wallet virtual wallet::PlatformKeyResult platformAccountReference(const wallet::IdentityAuthKey& key, const wallet::CompactXpub& compact_xpub) = 0; + //! Our DIP-15 receiving chain, as ensureFriendshipReceivingKeychain() + //! returns it, without importing it, and the birth of the one the wallet + //! holds. request.birth_time is not used. + virtual wallet::PlatformKeyResult getFriendshipReceivingXpub( + const wallet::FriendshipKeychainRequest& request) = 0; + //! Ensure our private DIP-15 receiving chain is imported as a ranged - //! descriptor and return the corresponding public chain. Derivation, - //! descriptor update and result publication are one wallet-locked action. + //! descriptor born no later than request.birth_time (in seconds) and + //! return the corresponding public chain. Derivation, descriptor update + //! and result publication are one wallet-locked action. A chain the + //! wallet holds whole, born no later, is left as it is. Importing the + //! chain, or moving its birth earlier, first records a rescan owed from + //! that birth (see owedRescanFrom()). If the import then fails + //! (IMPORT_ERROR), the chain may lack some of its scripts until it is + //! imported again, which owes a new rescan; until then no rescan settles + //! the one recorded, so a reload still owes it. The failed birth may not + //! have been saved: ask again with that birth or an earlier one. virtual wallet::PlatformKeyResult ensureFriendshipReceivingKeychain( const wallet::FriendshipKeychainRequest& request) = 0; + //! Every credit output of this wallet's asset lock transactions that pays + //! one of keys, derived from the transactions the wallet holds. Deriving + //! the keys needs an unlocked wallet. + virtual wallet::PlatformKeyResult> listAssetLockFundings( + const std::vector& keys) = 0; + //! Return whether wallet has private key. virtual bool isSpendable(const CScript& script) = 0; virtual bool isSpendable(const CTxDestination& dest) = 0; diff --git a/src/wallet/db.h b/src/wallet/db.h index 1921b104db12..978efe641bee 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -169,6 +169,9 @@ class WalletDatabase virtual std::string Format() = 0; std::atomic nUpdateCounter; + //! Whether a WalletBatch failed to write a record since this database was + //! opened, so the wallet may hold what it did not save. + std::atomic m_write_failed{false}; unsigned int nLastSeen{0}; unsigned int nLastFlushed{0}; int64_t nLastWalletUpdate{0}; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index c6d755e9d95d..9714c2b2b0cc 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -188,34 +188,30 @@ class WalletImpl : public Wallet { return m_wallet->ChangeWalletPassphrase(old_wallet_passphrase, new_wallet_passphrase); } - //! Where a rescan from the birth of the wallet's first key starts. - int rescanStartHeight() - { - int rescan_height{0}; - std::optional time_first_key; - for (auto spk_man : m_wallet->GetAllScriptPubKeyMans()) { - int64_t time = spk_man->GetTimeFirstKey(); - if (!time_first_key || time < *time_first_key) time_first_key = time; - } - if (time_first_key) { - m_wallet->chain().findFirstBlockWithTimeAndHeight(*time_first_key - TIMESTAMP_WINDOW, rescan_height, - FoundBlock().height(rescan_height)); + //! The block a rescan of the blocks a key born at `time` may be paid in + //! starts from, and its height, as importdescriptors does; nullopt when + //! no block is that recent. + std::optional> rescanStart(int64_t time) + { + uint256 hash; + int height{0}; + if (!m_wallet->chain().findFirstBlockWithTimeAndHeight(time - TIMESTAMP_WINDOW, /*min_height=*/0, + FoundBlock().hash(hash).height(height))) { + return std::nullopt; } - return rescan_height; + return std::make_pair(hash, height); } - wallet::RescanStatus startRescan(bool from_genesis) override + //! Scan from start and, once complete, settle the owed rescans up to + //! owed_sequence that it covers from covered_from. + wallet::RescanStatus rescanFrom(const WalletRescanReserver& reserver, const std::pair& start, + uint64_t owed_sequence, int64_t covered_from) { - const int rescan_height{from_genesis ? 0 : rescanStartHeight()}; - - WalletRescanReserver reserver(*m_wallet); - if (!reserver.reserve()) { - return wallet::RescanStatus::BUSY; - } - switch (m_wallet->ScanForWalletTransactions(m_wallet->chain().getBlockHash(rescan_height), rescan_height, /*max_height=*/std::nullopt, - reserver, /*fUpdate=*/true, /*save_progress=*/false).status) { + switch (m_wallet->ScanForWalletTransactions(start.first, start.second, /*max_height=*/std::nullopt, reserver, + /*fUpdate=*/true, /*save_progress=*/false).status) { case CWallet::ScanResult::FAILURE: return wallet::RescanStatus::FAILURE; case CWallet::ScanResult::SUCCESS: + WITH_LOCK(m_wallet->cs_wallet, m_wallet->SettleOwedRescans(owed_sequence, covered_from)); return wallet::RescanStatus::SUCCESS; case CWallet::ScanResult::USER_ABORT: return wallet::RescanStatus::USER_ABORT; @@ -223,14 +219,78 @@ class WalletImpl : public Wallet Assume(false); // unreachable return wallet::RescanStatus::FAILURE; // fallback for release builds } + wallet::RescanStatus startRescan(bool from_genesis) override + { + WalletRescanReserver reserver(*m_wallet); + if (!reserver.reserve()) { + return wallet::RescanStatus::BUSY; + } + uint64_t owed; + std::optional time_first_key; + { + LOCK(m_wallet->cs_wallet); + owed = m_wallet->GetOwedRescanSequence(); + if (!from_genesis) { + for (auto spk_man : m_wallet->GetAllScriptPubKeyMans()) { + int64_t time = spk_man->GetTimeFirstKey(); + if (!time_first_key || time < *time_first_key) time_first_key = time; + } + } + } + std::optional> start; + if (time_first_key) start = rescanStart(*time_first_key); + if (!start) start.emplace(m_wallet->chain().getBlockHash(0), 0); + return rescanFrom(reserver, *start, owed, time_first_key.value_or(0)); + } + wallet::RescanStatus rescanFromTime(int64_t from_time) override + { + from_time = std::max(from_time, 0); + WalletRescanReserver reserver(*m_wallet); + if (!reserver.reserve()) { + return wallet::RescanStatus::BUSY; + } + // Taken before looking for blocks: a chain imported later may be paid + // in one that arrives meanwhile. + const uint64_t owed{WITH_LOCK(m_wallet->cs_wallet, return m_wallet->GetOwedRescanSequence())}; + const auto start{rescanStart(from_time)}; + if (!start) { + LOCK(m_wallet->cs_wallet); + m_wallet->chain().requestMempoolTransactions(*m_wallet); + m_wallet->SettleOwedRescans(owed, from_time); + return wallet::RescanStatus::SUCCESS; + } + return rescanFrom(reserver, *start, owed, from_time); + } void abortRescan() override { m_wallet->AbortRescan(); } - std::optional rescanPrunedFrom() override + std::optional rescanUnavailableFrom(int64_t from_time) override + { + const auto start{rescanStart(std::max(from_time, 0))}; + if (!start) return std::nullopt; + // The rescan reads up to the chain's tip, which the wallet may not have reached. + const CBlockLocator locator{m_wallet->chain().getTipLocator()}; + int tip_height{0}; + if (locator.IsNull() || !m_wallet->chain().findBlock(locator.vHave.front(), FoundBlock().height(tip_height))) { + return std::nullopt; + } + const uint256& tip{locator.vHave.front()}; + if (m_wallet->chain().hasBlocks(tip, start->second)) return std::nullopt; + // Every block from start up to the first one without data has it. + int first_missing{start->second}; + int last{tip_height}; + while (first_missing < last) { + const int mid{first_missing + (last - first_missing) / 2}; + if (m_wallet->chain().hasBlocks(tip, start->second, mid)) { + first_missing = mid + 1; + } else { + last = mid; + } + } + return first_missing; + } + std::optional owedRescanFrom() override { - if (!m_wallet->chain().havePruned()) return std::nullopt; LOCK(m_wallet->cs_wallet); - const int rescan_height{rescanStartHeight()}; - if (m_wallet->chain().hasBlocks(m_wallet->GetLastBlockHash(), rescan_height)) return std::nullopt; - return rescan_height; + return m_wallet->GetOwedRescanFrom(); } void autoLockMasternodeCollaterals() override { m_wallet->AutoLockMasternodeCollaterals(); } bool backupWallet(const std::string& filename) override { return m_wallet->BackupWallet(filename); } @@ -281,11 +341,21 @@ class WalletImpl : public Wallet { return m_wallet->PlatformAccountReference(key, compact_xpub); } + wallet::PlatformKeyResult getFriendshipReceivingXpub( + const wallet::FriendshipKeychainRequest& request) override + { + return m_wallet->GetFriendshipReceivingXpub(request); + } wallet::PlatformKeyResult ensureFriendshipReceivingKeychain( const wallet::FriendshipKeychainRequest& request) override { return m_wallet->EnsureFriendshipReceivingKeychain(request); } + wallet::PlatformKeyResult> listAssetLockFundings( + const std::vector& keys) override + { + return m_wallet->ListAssetLockFundings(keys); + } bool isSpendable(const CScript& script) override { LOCK(m_wallet->cs_wallet); diff --git a/src/wallet/platformtypes.h b/src/wallet/platformtypes.h index 64aeb0b3f21e..027d3454ca33 100644 --- a/src/wallet/platformtypes.h +++ b/src/wallet/platformtypes.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_WALLET_PLATFORMTYPES_H #define BITCOIN_WALLET_PLATFORMTYPES_H +#include #include #include