Based on #10179, this effectively reverts #9583, regaining most of the original speedups of #7946.
This concludes the work of #9725, #10178, and #10179.
See individual commit messages for more information.
510@@ -511,6 +511,9 @@ class CTxMemPool
511 // to track size/count of descendant transactions. First version of
512 // addUnchecked can be used to have it call CalculateMemPoolAncestors(), and
513 // then invoke the second version.
514+ // Note that addUnchecked is ONLY called from ATMP during normal operation,
515+ // and any other callers may break wallet's in-mempool tracking (due to
516+ // lack of CValidationInterface::TransactionAddedToMempool callbacks).
In commit “Add a CValidationInterface::TransactionRemovedFromMempool”
What does this imply? Just that if there are any new calls to addUnchecked
, the caller also needs to signal TransactionAddedToMempool
not to break the wallet? Would say this in the comment explicitly if this is the case.
510@@ -511,6 +511,9 @@ class CTxMemPool
511 // to track size/count of descendant transactions. First version of
512 // addUnchecked can be used to have it call CalculateMemPoolAncestors(), and
513 // then invoke the second version.
514+ // Note that addUnchecked is ONLY called from ATMP during normal operation,
In commit “Add a CValidationInterface::TransactionRemovedFromMempool”
Unclear to me what a normal operation is. Comment might be clearer mentioning a not normal counterexample.
1184+ TRY_LOCK(cs_main, mainLocked);
1185+ if (mainLocked) {
1186+ if (this->lastBlockProcessed == chainActive.Tip()) {
1187+ return true;
1188+ }
1189+ // If the user called invalidatechain some things might block
In commit “Add CWallet::BlockUntilSyncedToCurrentChain()”
Does “some things might block forever” just mean this wait might block forever? If so, maybe be more concrete and say something like “lastBlockProcessed will not be rewound back to chainActive.Tip().” Otherwise it would be good to clarify what some things is referring to.
1116@@ -1108,6 +1117,14 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
1117 If possibleOldChain is provided, the parameters from the old chain (version)
1118 will be preserved. */
1119 bool SetHDMasterKey(const CPubKey& key, CHDChain *possibleOldChain = nullptr);
1120+
1121+ /**
1122+ * Blocks until the wallet state is up-to-date to /at least/ the current
1123+ * chain at the time this function is entered
1124+ * Obviously holding cs_main/cs_wallet when going into this call may cause
1125+ * deadlock
In commit “Add CWallet::BlockUntilSyncedToCurrentChain()”
Stray tab here
2711@@ -2648,6 +2712,10 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
2712
2713 RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR));
2714
2715+ // Make sure the results are valid at least up to the most recent block
In commit “Add calls to CWallet::BlockUntilSyncedToCurrentChain() in RPCs”
Can you give an example of specific bug that could occur without these BlockUntilSynced calls and is prevented by adding them? I looked at some of the old issues (#9584, #9148, etc), but they’re confusing and I don’t know how much of the information is up to date.
It would be great if BlockUntilSyncedToCurrentChain
had a comment that made it clearer when it does and doesn’t need to be called, and what consistency issues it is and isn’t supposed to solve.
Maybe there should also be a bullet point in the new RPC interface guidelines about what kind of consistency wallet RPCs are expected to have.
66+ blkhash = bytes_to_hex_str(body)
67+ else:
68+ assert_equal(topic, b"hashtx")
69
70 msg = self.zmqSubSocket.recv_multipart()
71 topic = msg[0]
In commit “Fix zmq tests now that txn/blocks are unordered”
Maybe assert msg[0] != topic
above this line to confirm actually receive distinct hashtx
and hashblock
messages (not two hashblocks).
utACK 6fb571977d9cc41793a594688e5071dd5bbd864d.
Code changes mostly seem great, though as you can tell from my comments I have a somewhat hazy understanding of the semantics and assumptions being made. A little more documentation would make everything clear, I think.
11@@ -12,6 +12,7 @@
12 #include "rpc/server.h"
20+ boost::signals2::signal<void (const CBlockIndex *, const std::shared_ptr<const CBlock>&)> NewPoWValidBlock;
21+};
22+
23 static CMainSignals g_signals;
24
25+CMainSignals::CMainSignals() {
This would be safer/faster/cleaner with : internals(new CMainSignalsInstance()) {}
instead of the body.
Initializer lists guarantee proper cross-thread visibility, otherwise you might init twice and have sharing issues.
42@@ -43,6 +43,9 @@ class CScheduler
43 // Call func at/after time t
44 void schedule(Function f, boost::chrono::system_clock::time_point t);
void schedule(Function f, boost::chrono::system_clock::time_point t = boost::chrono::system_clock::now());
36+ bool fCallbacksRunning = false;
37+
38+ void MaybeScheduleProcessQueue() {
39+ {
40+ LOCK(cs_callbacksPending);
41+ // Try to avoid scheduling too many copies here, but if we
This comment and issue can be avoided entirely if you move line 56 up to 46. After that lines 54 and 55 (which will be 55 and 56) can be removed.
I think this is right (line numbers apply to commit 8daf2439796dfdee41c1a32787e0ec9726daf6be). It also seems like you could eliminate the fCallbacksRunning variable if you change ProcessQueue to call pop_front after running the callback and condition the AddToProcessQueue schedule() call on the queue being previously empty.
@ryanofsky I believe the point is to avoid duplicate calls to the scheduler since it may be multi-threaded. So a call to AddToProcessQueue should not schedule() anything if we’re already scheduled; it should only schedule() if our previously scheduled function has completed execution (at least beyond the point of it calling schedule() again). Can’t see implementing that without knowing if fCallbacksRunning is true.
Still, the code does not guarantee single threaded execution in its current form due to the fCallbacksRunning state being set AFTER the call to schedule(). Being that all the locks are placed appropriately we might as well make this guarantee or else it seems like a bug because the SingleThreadedClient won’t be.
I’ll see if I can’t get a test showing this behavior.
@ryanofsky I believe the point is to avoid duplicate calls to the scheduler since it may be multi-threaded.
I know, this is why the second half of my suggestion was “condition the AddToProcessQueue schedule() call on the queue being previously empty.” Anyway, I don’t think Matt’s particularly interested in these simplifications, and it’s easier to communicate these changes as patches rather than english descriptions, so I’d rather just leave any simplifications to followup PRs.
Still, the code does not guarantee single threaded execution in its current form due to the fCallbacksRunning state being set AFTER the call to schedule()
The reason it works in its current form is because of the if (fCallbacksRunning) return;
line at the top of ProcessQueue()
Again, I don’t think the code in it’s current form is the simplest it could be, but it seems safe and easy to clean up later in a followup PR. Also this whole discussion really should be moved to #10179. #10286 is only building on the changes in #10179.
@ryanofsky I see that now, the extra check does prevent the execution.
Being that this is new code I wouldn’t call it a simplification. Here’s a patch of the proposed change, less logic with the same function: scheduler.patch.txt
which reads better if you rename fCallbacksRunning
to fCallbacksScheduled
and this patch:
scheduler.patch2.txt
which can be argued reduces code reuse but I think the readability is improved.
@mchrostowski hmm, really, I find that it decreases readability (though that may be NIH). It looks harder to reason about whether some callbacks might accidentally get missed to me.
(Other random note, we dont use tabs in our codebase, which your patch added).
@TheBlueMatt Well, in that case I feel like either patch gets funky, especially since the use of fCallbacksRunning becomes inconsistent if you apply the first patch without the second (unless some alternative name for fCallbacksRunning
works).
The extra safety check and inconsistency of scheduling bothers me but I wouldn’t expect it to actually cause issues so I have no grounds for objection.
I think my inquiry stemmed from it not being immediately apparent that ProcessQueue() only runs once and the extra check is just an extra check. Perhaps the “not a big deal” part of the comment could be “because ProcessQueue() already checks” for clarity, I would not have looked so deeply into the code except that I thought “not a big deal” meant “sometimes interweaving calls is okay.”
57+
58+ callback = callbacksPending.front();
59+ callbacksPending.pop_front();
60+ }
61+
62+ // RAII the setting of fCallbacksRunning and calling MaybeScheduleProcessQueue
try {
callback();
} catch(...) {
{
LOCK(cs_callbacksPending);
fCallbacksRunning = false;
}
MaybeScheduleProcessQueue();
}
Why not try{} catch{}?
My guess about this was that it allows the processqueue to take advantage of whatever error handling or reporting cscheduler provides, and to not have to repeat the finalization logic both inside and after the catch clause. Either approach seems fine to me, though.
67+ ~RAIICallbacksRunning() {
68+ {
69+ LOCK(instance->cs_callbacksPending);
70+ instance->fCallbacksRunning = false;
71+ }
72+ instance->MaybeScheduleProcessQueue();
Is this really what we want? A scheduler call for each callback? It does prevent starving any other scheduled tasks in case of a long queue, but it also generates a lot of lock contention which can be a performance killer.
Unless there is evidence of this queue processing messing with other scheduling I feel strongly we should avoid this design. It will be much harder to detect performance issues from lock contention than performance issues from the processing of a long queue.
I recommend replacing line 45 with a size() query, – the size (to account for your pop), and put the entire thing in a do {} while (size > 0);. You can then avoid calling MaybeScheduleProcessQueue()
inside of ProcessQueue()
itself.
Is this really what we want? A scheduler call for each callback?
I’m guessing the work done in any of these callbacks far outweighs the cost of scheduling one of them but I could be wrong.
Okay, sounds like blocking/starving is the biggest risk. Can’t disagree with that.
Feels like a bit of wasted work to call schedule on things you know are intended to execute now, but that’s just a performance (not important) concern. A bit surprising there is only one scheduler thread when the scheduler counts threads, but indeed it is only one.
88+ * Class used by CScheduler clients which may schedule multiple jobs
89+ * which are required to be run serially. Does not require such jobs
90+ * to be executed on the same thread, but no two jobs will be executed
91+ * at the same time.
92+ */
93+class CSingleThreadedSchedulerClient {
This is no longer a scheduler. It has one public method, void AddToProcessQueue(std::function<void (void)> func);
, which does not take any ‘schedule’ information.
This class is neat, more of a SingleThreadedExecutor that happens to use a scheduler to execute. Really its treating the scheduler as a thread pool.
I’m all for keeping this if it’s not named ‘scheduler’ and if a thread pool abstraction can be extracted from CScheduler then both this class and CScheduler can use that pool for execution. Also to consider, is this used anywhere else yet or is it expected to be used?
1112@@ -1104,6 +1113,14 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
1113 caller must ensure the current wallet version is correct before calling
1114 this function). */
1115 bool SetHDMasterKey(const CPubKey& key);
1116+
1117+ /**
1118+ * Blocks until the wallet state is up-to-date to /at least/ the current
1119+ * chain at the time this function is entered
1120+ * Obviously holding cs_main/cs_wallet when going into this call may cause
1152@@ -1147,6 +1153,50 @@ void CWallet::BlockDisconnected(const std::shared_ptr<const CBlock>& pblock) {
1153
1154
1155
1156+void CWallet::BlockUntilSyncedToCurrentChain() {
This method is concerning. It may be that it is being used in a safe manner but the method itself is quite dangerous. Preliminary observation suggests this can be called from both the command line RPC and JSON RPC at the same time but I don’t know how true this is.
Calling it from two different threads appears to be not okay, so it is “Not thread safe” and should likely be labeled as such (though I don’t see this as a standard in the project codebase so maybe that’s going a bit far).
1158+ {
1159+ LOCK(cs_main);
1160+ initialChainTip = chainActive.Tip();
1161+ }
1162+ AssertLockNotHeld(cs_main);
1163+ AssertLockNotHeld(cs_wallet);
boost::thread_specific_ptr<LockStack> lockstack;
Didn’t see that, makes perfect sense then.
103@@ -104,7 +104,9 @@ void UnregisterAllValidationInterfaces() {
104
105 void CMainSignals::MempoolEntryRemoved(CTransactionRef ptx, MemPoolRemovalReason reason) {
106 if (reason != MemPoolRemovalReason::BLOCK && reason != MemPoolRemovalReason::CONFLICT) {
107- internals->TransactionRemovedFromMempool(ptx);
108+ internals->schedulerClient.AddToProcessQueue([ptx, this] {
Overall appears to be on a good track. It looks to me like the global lock (cs_main) is causing some serious confusion/issues and there is some general mistake in the pattern of locks or their encapsulation that makes this all difficult.
I reviewed everything pretty closely aside from the ZMQ test changes, that went over my head.
Please don’t overlook the outdated validationinterface.cpp comments, those took some time to put together.
567@@ -568,3 +568,16 @@ A few guidelines for introducing and reviewing new RPC interfaces:
568 - *Rationale*: as well as complicating the implementation and interfering
569 with the introduction of multi-wallet, wallet and non-wallet code should be
570 separated to avoid introducing circular dependencies between code units.
571+
572+- Wallet RPCs call BlockUntilSyncedToCurrentChain to maintain consistency with
573+ `getblockchaininfo`'s state immediately prior to the call's execution. Wallet
574+ RPCs who's behavior does *not* depend on the current chainstate may omit this
In commit “Add a dev notes document describing the new wallet RPC blocking”
s/who’s/whose
575+ call.
576+
577+ - *Rationale*: In previous versions of Bitcoin Core, the wallet was always
578+ in-sync with the chainstate (by virtue of them all being updated in the
579+ same cs_main lock). In order to maintain the behavior that wallet RPCs
580+ return restults as of at least the highest best-known-block an RPC
In commit “Add a dev notes document describing the new wallet RPC blocking”
s/restults/results, s/best-known-block/best-known block,/
So far looks good to me, I’m going to poke around wallet<->blockchain interaction so I can better understand the wallet.cpp changes you made before I comment further.
That said I feel like there is something fundamentally wrong with the interaction between CWallet and the blockchain (I don’t even know where that code lives yet). This feels like a solution to current issues but I would hope a refactor prevents needing such frequently occurring checks and thread specific execution.
That said, it would be interesting if a single “blockchain operations” thread is needed to get the code working in a reasonable manner. GUI systems tend to need such threads because of the nature of problem which is events coming from both ends of the system (rendering thread vs input thread) which would have to, in any logical design, invert lock ordering or else excessively block.
utACK 943217460bc527c4003868415c264e4a77a6e55a.
Changes since previous ACK were rebasing, adding developer notes, tweaking some comments, adding a check to the zmq test
@TheBlueMatt The stated purpose of this PR is to reduce locking on cs_main so as to reducing code coupling. I see one change in this PR that actually deletes a LOCK(cs_main)
which is in CWallet::InMempool()
. This looks like a step in the right direction.
That said, the remaining changes seem to be all about getting the signals into a background thread. What does this gain us for decoupling?
If the purpose is to not hold cs_main from whatever call sites hit GetMainSignals()
then I don’t see a benefit in using new threads when we can release the lock before making the call. That is, running in another thread is not decoupling synchronization or interface dependencies. The goal of “move wallet updates out of cs_main into a background thread” seems unrelated to decoupling because “using a background thread” and “not holding cs_main” are not dependent on each other, at least in the cases I observed.
Of particular concern is the re-ordering of calls, which you’re avoiding with the single threaded scheduler, but if this isn’t required for decoupling then it is just added complexity and overhead.
1184+ return true;
1185+ }
1186+ // If the user called invalidateblock our wait here might block
1187+ // forever, so we check if we're ahead of the tip (a state
1188+ // which should otherwise never be exposed outside of validation)
1189+ return this->lastBlockProcessed->nChainWork > chainActive.Tip()->nChainWork;
In commit “Add CWallet::BlockUntilSyncedToCurrentChain()”
I don’t understand why we would wait forever without this check. Does InvalidateBlock not trigger notifications that would lead to lastBlockProcessed being updated? And if it doesn’t, shouldn’t this just be fixed so the right notifications are sent?
1176+ // moved past initialChainTip through a reorg before we could get
1177+ // lastBlockProcessedMutex.
1178+ // This should be exceedingly rare in regular usage, so potentially
1179+ // eating 100ms to retry this lock should be fine (not TRY_LOCKing
1180+ // here would be a lock inversion against lastBlockProcessedMutex)
1181+ TRY_LOCK(cs_main, mainLocked);
In commit “Add CWallet::BlockUntilSyncedToCurrentChain()”
Maybe consider dropping the try-lock and replacing it with lastBlockProcessedMutex.unlock(); LOCK(cs_main); lastBlockProcessedMutex.lock();
. Maybe this would be a little slower in the average case where this code runs (which is rare to begin with), but it would avoid the 100ms worst case, and make the code simpler because you could also replace the while loop and timeout with a plain cv.wait(lock, pred)
call.
101@@ -102,6 +102,10 @@ void UnregisterAllValidationInterfaces() {
102 g_signals.m_internals->NewPoWValidBlock.disconnect_all_slots();
103 }
104
105+void CallFunctionInValidationInterfaceQueue(std::function<void ()> func) {
106+ g_signals.m_internals->m_schedulerClient.AddToProcessQueue(func);
In commit “Add CallFunctionInQueue to wait on validation interface queue drain”
Would std::move(func)
1164+ // Skip the queue-draining stuff if we know we're caught up with
1165+ // chainActive.Tip()
1166+ LOCK(cs_main);
1167+ const CBlockIndex* initialChainTip = chainActive.Tip();
1168+
1169+ if (m_last_block_processed == initialChainTip) {
In commit “Add CWallet::BlockUntilSyncedToCurrentChain()”
Maybe drop this check. Seems to be a special case of the check below which isn’t actually more expensive.
1172+ if (m_last_block_processed->GetAncestor(initialChainTip->nHeight) == initialChainTip) {
1173+ return;
1174+ }
1175+ }
1176+
1177+ std::condition_variable callbacks_done_cv;
In commit “Add CWallet::BlockUntilSyncedToCurrentChain()”
I think all the mutex/cv/lambda/looping stuff below could be replaced by:
0std::promise<void> promise;
1CallFunctionInValidationInterfaceQueue([&promise]() { promise.set_value(); });
2promise.get_future().wait();
885+ // If wallet is enabled, ensure that the wallet has been made aware
886+ // of the new transaction prior to returning. This prevents a race
887+ // where a user might call sendrawtransaction with a transaction
888+ // to/from their wallet, immediately call some wallet RPC, and get
889+ // a stale result because callbacks have not yet been processed.
890+ pwalletMain->TransactionAddedToMempool(tx);
In commit “Fix wallet RPC race by informing wallet of tx in sendrawtransaction”
Seems like this will result in the wallet getting two TransactionAddedToMempool notifications, which is fine but might be worth noting in the comment.
Also, not asking for this change, but would another way to do this without referencing the wallet here be to release cs_main and then wait for the other notification to be processed? (Maybe using CallFunctionInValidationInterfaceQueue like in BlockUntilSyncedToCurrentChain?)
155@@ -156,6 +156,15 @@ void AssertLockHeldInternal(const char* pszName, const char* pszFile, int nLine,
156 abort();
157 }
158
159+void AssertLockNotHeldInternal(const char* pszName, const char* pszFile, int nLine, void* cs)
160+{
161+ BOOST_FOREACH (const PAIRTYPE(void*, CLockLocation) & i, *lockstack)
In commit “Add ability to assert a lock is not held in DEBUG_LOCKORDER”
Looks like there are travis errors compiling this code (undefined FOREACH/PAIRTYPE).
891+ // a stale result because callbacks have not yet been processed.
892+ std::promise<void> promise;
893+ CallFunctionInValidationInterfaceQueue([&promise] {
894+ promise.set_value();
895+ });
896+ promise.get_future().wait();
In commit “Fix wallet RPC race by waiting for callbacks in sendrawtransaction”
I think you might need to release cs_main before waiting for the promise, because the wallet handler in the notification thread will want to acquire it.
FWIW, the way I implemented this in my wallet ipc branch was to call CallFunctionInValidationInterfaceQueue
the same way you are doing here, but release cs_main before the PushInventory
calls, and wait for the promise after the PushInventory
calls. I also updated the comment.
0--- a/src/rpc/rawtransaction.cpp
1+++ b/src/rpc/rawtransaction.cpp
2@@ -29,6 +29,7 @@
3 #include "wallet/wallet.h"
4 #endif
5
6+#include <future>
7 #include <stdint.h>
8
9 #include <univalue.h>
10@@ -846,7 +847,6 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
11 + HelpExampleRpc("sendrawtransaction", "\"signedhex\"")
12 );
13
14- LOCK(cs_main);
15 RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VBOOL});
16
17 // parse hex string from parameter
18@@ -860,6 +860,9 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
19 if (request.params.size() > 1 && request.params[1].get_bool())
20 nMaxRawTxFee = 0;
21
22+ std::promise<void> sent_notifications;
23+ {
24+ LOCK(cs_main);
25 CCoinsViewCache &view = *pcoinsTip;
26 bool fHaveChain = false;
27 for (size_t o = 0; !fHaveChain && o < tx->vout.size(); o++) {
28@@ -881,15 +884,6 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
29 }
30 throw JSONRPCError(RPC_TRANSACTION_ERROR, state.GetRejectReason());
31 }
32-#ifdef ENABLE_WALLET
33- } else {
34- // If wallet is enabled, ensure that the wallet has been made aware
35- // of the new transaction prior to returning. This prevents a race
36- // where a user might call sendrawtransaction with a transaction
37- // to/from their wallet, immediately call some wallet RPC, and get
38- // a stale result because callbacks have not yet been processed.
39- pwalletMain->TransactionAddedToMempool(tx);
40-#endif
41 }
42 } else if (fHaveChain) {
43 throw JSONRPCError(RPC_TRANSACTION_ALREADY_IN_CHAIN, "transaction already in block chain");
44@@ -897,11 +891,22 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
45 if(!g_connman)
46 throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Peer-to-peer functionality missing or disabled");
47
48+ CallFunctionInValidationInterfaceQueue([&sent_notifications] { sent_notifications.set_value(); });
49+ } // LOCK(cs_main)
50+
51 CInv inv(MSG_TX, hashTx);
52 g_connman->ForEachNode([&inv](CNode* pnode)
53 {
54 pnode->PushInventory(inv);
55 });
56+
57+ // Wait for any TransactionAddedToMempool notifications sent above to be
58+ // processed by thw wallet. This prevents a race where a user might call
59+ // sendrawtransaction with a transaction to/from their wallet, immediately
60+ // call some wallet RPC, and get a stale result because callbacks have not
61+ // yet been processed.
62+ sent_notifications.get_future().wait();
63+
64 return hashTx.GetHex();
65 }
912+ // a stale result because callbacks have not yet been processed.
913+ std::promise<void> promise;
914+ CallFunctionInValidationInterfaceQueue([&promise] {
915+ promise.set_value();
916+ });
917+ promise.get_future().wait();
In commit “Fix wallet RPC race by waiting for callbacks in sendrawtransaction”
This seems right. Possible tweaks:
should_wait_on_validationinterface
seems like it is always true at this point, maybe the variable is not needed.CallFunctionInValidationInterfaceQueue
before releasing cs_main, to avoid waiting for notifications that might be queued in the meantime that we don’t care about.901 throw JSONRPCError(RPC_TRANSACTION_ALREADY_IN_CHAIN, "transaction already in block chain");
902 }
903+
904+ } // cs_main
905+
906+ promise.get_future().wait();
In commit “Fix wallet RPC race by waiting for callbacks in sendrawtransaction”
Might be more efficient to wait for the promise after the PushInventory calls so they aren’t blocked waiting for wallets.
m_last_block_processed
. Should that be rewound in DisconnectBlock()
?
96@@ -85,10 +97,17 @@ void UnregisterAllValidationInterfaces() {
97 g_signals.m_internals->TransactionAddedToMempool.disconnect_all_slots();
98 g_signals.m_internals->BlockConnected.disconnect_all_slots();
99 g_signals.m_internals->BlockDisconnected.disconnect_all_slots();
100+ g_signals.m_internals->TransactionRemovedFromMempool.disconnect_all_slots();
101 g_signals.m_internals->UpdatedBlockTip.disconnect_all_slots();
102 g_signals.m_internals->NewPoWValidBlock.disconnect_all_slots();
103 }
104
105+void CMainSignals::MempoolEntryRemoved(CTransactionRef ptx, MemPoolRemovalReason reason) {
const CTransactionRef& ptx
, like TransactionAddedToMempool
?
101 g_signals.m_internals->UpdatedBlockTip.disconnect_all_slots();
102 g_signals.m_internals->NewPoWValidBlock.disconnect_all_slots();
103 }
104
105+void CMainSignals::MempoolEntryRemoved(CTransactionRef ptx, MemPoolRemovalReason reason) {
106+ if (reason != MemPoolRemovalReason::BLOCK && reason != MemPoolRemovalReason::CONFLICT) {
1184+ return;
1185+ }
1186+ }
1187+
1188+ std::promise<void> promise;
1189+ CallFunctionInValidationInterfaceQueue([&promise] {
334@@ -337,6 +335,7 @@ class CWalletTx : public CMerkleTx
335 mutable CAmount nImmatureWatchCreditCached;
336 mutable CAmount nAvailableWatchCreditCached;
337 mutable CAmount nChangeCached;
338+ mutable bool fInMempool;
fInMempool
for each wallet transaction, maybe a std::set<WalletTx*>
with the mempool transactions takes less memory?
74@@ -75,6 +75,9 @@ UniValue getinfo(const JSONRPCRequest& request)
75 #ifdef ENABLE_WALLET
76 CWallet * const pwallet = GetWalletForJSONRPCRequest(request);
77
78+ if (pwallet) {
EnsureWalletIsAvailable
?
4395+ bool ret = ::AcceptToMemoryPool(mempool, state, tx, true, nullptr, nullptr, false, nAbsurdFee);
4396+ fInMempool = ret;
4397+ return ret;
4398+}
4399+
4400+bool CalculateEstimateType(FeeEstimateMode mode, bool opt_in_rbf) {
In commit “Use callbacks to cache whether wallet transactions are in mempool”
Looks like you accidentally resurrected this CalculateEstimateType function in the rebase. Should remove.
6@@ -7,6 +7,7 @@
7 #define BITCOIN_VALIDATIONINTERFACE_H
8
9 #include <memory>
10+#include <functional>
723@@ -724,6 +724,14 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
724
725 std::unique_ptr<CWalletDBWrapper> dbw;
726
727+ /**
728+ * The following are used to keep track of how far behind the wallet is
3894@@ -3869,6 +3895,12 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile)
3895 if (walletdb.ReadBestBlock(locator))
3896 pindexRescan = FindForkInGlobalIndex(chainActive, locator);
3897 }
3898+
3899+ //We must set m_last_block_processed prior to registering the wallet as a validation interface
//
31@@ -31,6 +32,11 @@ void RegisterValidationInterface(CValidationInterface* pwalletIn);
32 void UnregisterValidationInterface(CValidationInterface* pwalletIn);
33 /** Unregister all wallets from core */
34 void UnregisterAllValidationInterfaces();
35+/**
36+ * Pushes a function to callback onto the notification queue, guaranteeing any
37+ * callbacks generated prior to now are finished when the function is called.
38+ */
39+void CallFunctionInValidationInterfaceQueue(std::function<void ()> func);
I think a better interface would be
std::future<void> SyncWithValidationInterfaceQueue();
Whose implementation sets up the delayed call to a promise.set_value()
, instead of this being the responsibility of the caller as it is now.
The sync functionality is the only purpose, so rather than have an overly-generic interface which allows calling any function, it’s better to have a more specific interface which simplifies the interface as well as the setup work on the caller side.
I think most of the use-cases for now could almost do fine with a future-like return value if
std::future
allowed you to query whether the future had yet completed.
Wouldn’t std::future::wait_for()
with argument provide that functionality?
Orthogonally, if the intended interface really is to allow running arbitrary functions on the queue, than the current interface is fine. If it’s just to sync with it, I think the interface I suggested is better.
std::future
is done, without waiting.
2856@@ -2769,6 +2857,10 @@ UniValue listunspent(const JSONRPCRequest& request)
2857 nMaximumCount = options["maximumCount"].get_int64();
2858 }
2859
2860+ // Make sure the results are valid at least up to the most recent block
2861+ // the user could have gotten from another RPC command prior to now
2862+ pwallet->BlockUntilSyncedToCurrentChain();
assert(pwallet != nullptr);
BlockConnectedDisconnected
in comments and commit messages should say BlockConnected/Disconnected
, or Block[Connected|Disconnected]`, or something of the sort. The way it’s written like now looks like there’s an actual single function by that name.142
143 void CMainSignals::SetBestChain(const CBlockLocator &locator) {
144- m_internals->SetBestChain(locator);
145+ m_internals->m_schedulerClient.AddToProcessQueue([locator, this] {
146+ m_internals->SetBestChain(locator);
147+ });
CMainSignals::SetBestChain()
might be called with a CBlockLocator
object argument which goes out of scope and gets destructed before the queued m_internals->SetBestChain()
is called, causing an invalid memory access. For example, this can happen in the walletInstance->SetBestChain(chainActive.GetLocator());
call in wallet.cpp).
31@@ -31,6 +32,7 @@
32 #endif
33
34 #include <stdint.h>
35+#include <future>
CallFunctionInValidationInterfaceQueue
interface can be improved, detailed further in a comment above.
1276+ return;
1277+ }
1278+ }
1279+
1280+ std::promise<void> promise;
1281+ CallFunctionInValidationInterfaceQueue([&promise] {
This is explained here: #10286 (comment)
it now just tests if it is caught up, and if it is not, it puts a callback into the CValidationInterface queue and waits for it to trigger
I think this should be a code comment rather than a github comment!
114+ }
115+}
116+
117 void CMainSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) {
118- m_internals->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload);
119+ m_internals->m_schedulerClient.AddToProcessQueue([pindexNew, pindexFork, fInitialDownload, this] {
nTimeBestReceived
which gets updated to the current time in this callback. Now that the callback is asynchronous, it won’t necessarily be updated immediately. Does that cause a problem when nTimeBestReceived
is used in the Broadcast
callback?
104
105+void CallFunctionInValidationInterfaceQueue(std::function<void ()> func) {
106+ g_signals.m_internals->m_schedulerClient.AddToProcessQueue(std::move(func));
107+}
108+
109+void CMainSignals::MempoolEntryRemoved(CTransactionRef ptx, MemPoolRemovalReason reason) {
TransactionRemovedFromMempool()
but the signal is named MempoolEntryRemoved
. For all other callbacks in the validation interface, the callback name matches the signal name.
724@@ -722,6 +725,14 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
725
726 std::unique_ptr<CWalletDBWrapper> dbw;
727
728+ /**
729+ * The following is used to keep track of how far behind the wallet is
730+ * from the chain sync, and to allow clients to block on us being caught up
731+ *
732+ * Protected by cs_main
1665@@ -1625,6 +1666,8 @@ CBlockIndex* CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool f
1666 ShowProgress(_("Rescanning..."), 100); // hide progress dialog in GUI
1667
1668 fScanningWallet = false;
1669+
1670+ m_last_block_processed = chainActive.Tip();
Yes - I agree. m_last_block_processed
would be a really nice value to expose in the getwalletinfo
RPC.
I have a weak preference to make m_last_block_processed
work as you’d expect (tracking the last block processed by the wallet), but this PR is already doing a lot, so I think it’s also fine to keep this as is. Please update the comment in wallet.h to say that it’s not actually tracking how far the wallet is behind chain sync and shouldn’t be publicly exposed until that’s changed.
918@@ -917,14 +919,19 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
919 );
920
921 ObserveSafeMode();
922+
923+ CTransactionRef tx;
924+ std::promise<void> promise;
925+
926+ { // cs_main scope
CTransactionRef
above and then recompute the GetHash()
at the end of this function?
Yes, sorry - my comment was unclear.
Change looks good.
This is currently unused, but will by used by wallet to cache when
transactions are in the mempool, obviating the need for calls to
mempool from CWalletTx::InMempool()
This is both good practice (we want to move all such callbacks
into a background thread eventually) and prevents a lock inversion
when we go to use this in wallet (mempool.cs->cs_wallet and
cs_wallet->mempool.cs would otherwise both be used).
This blocks until the wallet has synced up to the current height.
This prevents the wallet-RPCs-return-stale-info issue from being
re-introduced when new-block callbacks no longer happen in the
block-connection cs_main lock
This avoid calling out to mempool state during coin selection,
balance calculation, etc. In the next commit we ensure all wallet
callbacks from CValidationInterface happen in the same queue,
serialized with each other. This helps to avoid re-introducing one
of the issues described in #9584 [1] by further disconnecting
wallet from current chain/mempool state.
Thanks to @morcos for the suggestion to do this.
Note that there are several race conditions introduced here:
* If a user calls sendrawtransaction from RPC, adding a
transaction which is "trusted" (ie from them) and pays them
change, it may not be immediately used by coin selection until
the notification callbacks finish running. No such race is
introduced in normal transaction-sending RPCs as this case is
explicitly handled.
* Until Block{Connected,Disconnected} and
TransactionAddedToMempool calls also run in the CSceduler
background thread, there is a race where
TransactionAddedToMempool might be called after a
Block{Connected,Disconnected} call happens.
* Wallet will write a new best chain from the SetBestChain
callback prior to having processed the transaction from that
block.
[1] "you could go to select coins, need to use 0-conf change, but
such 0-conf change may have been included in a block who's
callbacks have not yet been processed - resulting in thinking they
are not in mempool and, thus, not selectable."
This runs Block{Connected,Disconnected}, SetBestChain, Inventory,
and TransactionAddedToMempool on the background scheduler thread.
Of those, only BlockConnected is used outside of Wallet/ZMQ, and
is used only for orphan transaction removal in net_processing,
something which does not need to be synchronous with anything
else.
This partially reverts #9583, re-enabling some of the gains from
#7946. This does not, however, re-enable the gains achieved by
repeatedly releasing cs_main between each transaction processed.
Note that UpdatedBlockTip is also used in net_processing to
announce new blocks to peers. As this may need additional review,
this change is included in its own commit.
1271+
1272+ std::promise<void> promise;
1273+ CallFunctionInValidationInterfaceQueue([&promise] {
1274+ promise.set_value();
1275+ });
1276+ promise.get_future().wait();
3943@@ -3913,6 +3944,10 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile)
3944 if (walletdb.ReadBestBlock(locator))
3945 pindexRescan = FindForkInGlobalIndex(chainActive, locator);
3946 }
3947+
3948+ walletInstance->m_last_block_processed = chainActive.Tip();
Yes, according to my annotations (#11226) calling CreateWalletFromFile
requires holding cs_main
:
0src/wallet/wallet.h: static CWallet* CreateWalletFromFile(const std::string walletFile)
1 EXCLUSIVE_LOCKS_REQUIRED(cs_main);
This is due to the following underlying locking requirements:
CreateWalletFromFile
is reading the variable chainActive
which requires holding the mutex cs_main
.CreateWalletFromFile
calls FindForkInGlobalIndex
which reads the variable mapBlockIndex
. Reading the variable mapBlockIndex
requires holding the mutex cs_main
.3023@@ -3010,14 +3024,18 @@ bool CWallet::CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey, CCon
3024 // Track how many getdata requests our transaction gets
3025 mapRequestCount[wtxNew.GetHash()] = 0;
3026
3027+ // Get the inserted-CWalletTx from mapWallet so that the
3028+ // fInMempool flag is cached properly
3029+ CWalletTx& wtx = mapWallet[wtxNew.GetHash()];
264@@ -265,6 +265,7 @@ void Shutdown()
265 #endif
266 UnregisterAllValidationInterfaces();
267 GetMainSignals().UnregisterBackgroundSignalScheduler();
268+ GetMainSignals().UnregisterWithMempoolSignals(mempool);
UnregisterBackgroundSignalScheduler
?