Currently, by default if a wallet is fallen-behind from Chainstate tip, it will ask Chain interface to send back blocks to verify the confirmation state of its transactions. This behavior is really likely to occur after every node shutdown for half an hour. Rescan logic may also occur when a privkey, pubkey or address is devoid of timestamp and you need to rescan from 0 to be sure you don't miss any transactions.
Rescan logic is heavily relying on short lived LOCK(cs_main) for knowing the current height, guessing scanning progress, fetching the block, deciding to stop the rescan. Moving locks inside the Chain interface, doesn't solve the problem and if this API is more exposed in the future, you just make an easy way to bother Chainstate operations. It's inefficient, specially in case of multiple wallets where rescans are going to be in concurrency with each other.
Ideally, you want to serve multiple clients in parallel without locking the chain.
We craft a new data structure, ChainServer, with the following members :
std::list<ChainClient> ChainClients, a list of clients to servestd::unordered_map<uint256, CBlock> CachedBlocksa tree of recent blocksThreadPoolChainServer control, a control structure to coordinate clients service
ChainServer is implementing the CValidationInterface and is the solely receiver of BlockConnected/BlockDisconnected events.
When a new block is received, it's cached in CachedBlocks. In case of reorgs, we keep track of forks in cache too.
At ChainClient initialization, it registers beside to ChainServer using a reworked Chain::handleNotifications call and passing its m_last_block_processed. They may also pass a unix timestamp with conversion to height being done on the node side.
Our worker threads, read the lowest tip of one of ChainClients and start to serve block to it, by reading the cache or ReadBlockFromDisk if needed.
The thread may yield after X blocks servicing to avoid long starvation on the ChainClient side, and one being a Tip - 10, being blocked by others being at height 0 and waiting for them reaching Tip forever.
If thread detects ChainClient Tip being on a fork, it should roll back reorged blocks until common ancestor and connect forward the block until tip. If hash of reorged block isn't in CachedBlocks, thread should return an error code to let wallet flush confirmation state of all its transaction then restart rescan from 0.
If ChainClient is a new blank one, it should be pass a SPECIAL_VALUE to do a "fast-forward" and avoid replaying old blocks for a wallet which doesn't care because it has no transactions.
When a privkey, pubkey or address is imported, RPC call should trigger a new Chain::handleNotifications call with height of timestamp is provided or zero if not. A further optimization would be to pass rescan range but needs better height tracking in descriptors or address format, so that's outside of scope.
- what's the best size of block cache and being sure there is no gap with blocks being on disk ?
- what's the best size of threadpool, are people using 10 wallets same time rn ?
- how to turn block cache as a headers chain to increase is size by order of magnitude and avoid cache miss ?
- should we spawn/assign a thread by client or any worker can server any clients ?
- should we be able to split locks as Read for Worker and Write for
BlockConnectedon theCachedBlocksto allow concurrent read ?
interfaces::Chain
________
_________ | broadcastTransaction(Tx) | |
| |-------------------------------------------->| ATMP() |
| Wallet3 | | TransactionAddedToMempool() |________|
|_________|<-----------------------------------------------------------------------------------\
| \
\
| ChainServer \
__________________________________________ \
| | ________________________ | \
| | | | \
| | | std::list<ChainClient> | | \
_________ | |________________________| | \
| | RegisterClientWithTip(hashBlock) | _____________________________________ | \
| Wallet2 |------------------------------------>| | | | \
|_________| | | | std::unordered_map<uint256, CBlock> | |<---------- Scheduler
| |_____________________________________| |
| | _______________________ |
_________ | | | |
| | BlockConnected/Disconnected | | ThreadPoolChainServer | |
| Wallet1 |<------------------------------------| |_______________________| |
|_________| | |__________________________________________|
|
Lots of feedback. Small things first:
Ideally, whatever solution we come up for wallet rescans should be reuseable by indexes as well. Indexes have their own block reading loop in
BaseIndex::ThreadSyncwhich operates out of sequence with the wallets. It'd be nice if new chain rescanning code we write for wallets could be compatible with indexes as well. For example, it might be nice to send a more generalRewind(old_tip, new_tip)type of notification instead of justBlockDisconnected(block).I think most of the places this proposal is referring to
ChainClientinterface, it should be probably referring to theChain::Notificationsinterface andNotificationsHandlerImplinstead.ChainClientis to meant to be responsible for starting and stopping components during init and shutdown. It doesn't really have to do with notifications or chain state. (It would probably make sense to renameChainClienttoNodeClientor something like that.) There is also not necessarily a 1:1 correspondence betweenChainClientandChain::Notificationsobjects. For example, the multiwallet implementation in #10102 uses individualChainNotificationobjects for each wallet but just one sharedChainClientobject for all wallets right now.The proposed
ChainServerclass seems redundant with the existingChainclass. I understand a two way split betweenChain/Chain::Notificationsfor bidirectional communication, but not a three way split betweenChain/ChainClient/ChainServer. TheCachedBlocksmap also seems duplicative withg_blockman.m_block_indexand theThreadPoolChainServerclass may be adding functionality that more properly belongs inMainSignalsInstance(the class responsible for asynchronously sending notifications from a queue).More significantly, I think this proposal could be clearer about the exact problems it is solving, and try to take an more incremental approach to solving them to avoid rewriting too much code at once.
It seems like there are two problems this proposal solves, and the first is clear and straightforward, while the second is more open ended and messy.
BlockConnected/BlockDisconnectednotifications, so wallets are blocked when they could be doing work.I'd suggest solving problem (1) by itself first because it's kind of a prerequisite for solving the problem (2), and it is simpler.
My proposal would be to first write some rescan code that can take multiple
CBlockLocatorscan requests and consolidate them into a single scan that sends notifications to all the relevant handler. I wrote some pseudocode below that ignores locking, but there is very similar real world code already written inBaseIndex::ThreadSync.I'd would put this in
src/node/rescan.h/src/node/rescan.cppand use it to replace wallet and BaseIndex rescan code. Steps to use this would be to:node::Rescan m_rescanmember toChainImplCBlockLocator locatorargument to theChain::handleNotifications()method and have it callm_rescan.addRequest()instead ofRegisterValidationInterface()like currently.CreateWalletFromFileand have it just pass its locator to the existingChain::handleNotificationscall thereChain::startNotificationsmethod which starts a worker thread if one isn't one currently running, and to callm_rescan.sendNotifications()from that thread.Chain::startNotificationscalls where appropriate at the end ofAppInitMain, and inloadwallet/createwalletRPC calls.This would take care of the repeated rescan problem (1) above.
Beyond that we could:
importmulti,rescanblockchain, etc wallet RPC requests that use start and end blocks instead of locators.BaseIndexand useChain::handleNotificationsinstead there