@@ -678,6 +678,33 @@ void CCoinJoinClientManager::UpdatedSuccessBlock()
678678 nCachedLastSuccessBlock = nCachedBlockHeight;
679679}
680680
681+ // NOTE: the record which owns the locks and the persistent locks themselves are
682+ // separate writes and each write is its own implicit transaction, so commit them in
683+ // one explicit database transaction: a crash in between must neither leave
684+ // persistently locked coins behind with nothing tracking them (nothing would ever
685+ // release them again) nor persist the record without its locks (on restart the
686+ // missing lock reads as a manual unlock, the entry is dropped and the input becomes
687+ // selectable again while the finalized mixing transaction may still be in flight).
688+ // If no transaction can be started, don't persist anything rather than risk exactly
689+ // those partial states. The locks of all entries are written, not only of the ones
690+ // just added: entries added while nothing could be persisted are locked in memory only.
691+ // A lock released manually in the meantime is left alone, the next check drops its entry.
692+ static bool PersistPendingObservations (const CWallet& wallet, wallet::WalletBatch& batch,
693+ const std::map<COutPoint, int64_t >& pending)
694+ EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
695+ {
696+ if (!batch.TxnBegin ()) return false ;
697+ bool fPersisted {batch.WriteCoinJoinPendingObs (pending)};
698+ for (auto it = pending.begin (); fPersisted && it != pending.end (); ++it) {
699+ if (wallet.IsLockedCoin (it->first )) fPersisted = batch.WriteLockedUTXO (it->first );
700+ }
701+ if (!fPersisted ) {
702+ batch.TxnAbort ();
703+ return false ;
704+ }
705+ return batch.TxnCommit ();
706+ }
707+
681708void CCoinJoinClientManager::AddPendingObservation (const std::vector<COutPoint>& outpoints)
682709{
683710 AssertLockNotHeld (cs_pending_obs);
@@ -698,54 +725,19 @@ void CCoinJoinClientManager::AddPendingObservation(const std::vector<COutPoint>&
698725 WalletCJLogPrint (m_wallet, " CCoinJoinClientManager::%s -- %s is locked until the finalized mixing transaction is observed\n " ,
699726 __func__, outpoint.ToStringShort ());
700727 }
701- if (!PersistPendingObservations (batch)) {
728+ // Never overwrite a record which could not be read (LoadPendingObservations() left
729+ // m_pending_obs_loaded unset), that would permanently orphan the locks it still tracks
730+ if (!m_pending_obs_loaded || !PersistPendingObservations (*m_wallet, batch, m_pending_obs)) {
702731 // The in-memory lock still protects these inputs for as long as this process
703- // runs, but a restart before CheckPendingObservations() manages to persist them
704- // would make them selectable again while a valid mixing transaction spending them
705- // may already be in flight. Nothing more we can do about it here beyond making
706- // the failure loud - the wallet database is broken.
732+ // runs, but a restart would make them selectable again while a valid mixing
733+ // transaction spending them may already be in flight. Nothing we can do about
734+ // it here beyond making the failure loud - the wallet database is broken.
707735 LogPrintf (" CCoinJoinClientManager::%s -- ERROR: failed to persist locks for %d successfully mixed input(s), " /* Continued */
708- " they will not survive a restart until this succeeds \n " ,
736+ " they will not survive a restart\n " ,
709737 __func__, outpoints.size ());
710738 }
711739}
712740
713- bool CCoinJoinClientManager::PersistPendingObservations (wallet::WalletBatch& batch)
714- {
715- AssertLockHeld (m_wallet->cs_wallet );
716- AssertLockHeld (cs_pending_obs);
717-
718- // NOTE: the record which owns the locks and the persistent locks themselves are
719- // separate writes and each write is its own implicit transaction, so commit them in
720- // one explicit database transaction: a crash in between must neither leave
721- // persistently locked coins behind with nothing tracking them (nothing would ever
722- // release them again) nor persist the record without its locks (on restart the
723- // missing lock reads as a manual unlock, the entry is dropped and the input becomes
724- // selectable again while the finalized mixing transaction may still be in flight).
725- // If no transaction can be started, don't persist anything rather than risk exactly
726- // those partial states. Likewise if the existing record could not be read
727- // (LoadPendingObservations() left m_pending_obs_loaded unset): overwriting it would
728- // permanently orphan the locks it still tracks.
729- if (!m_pending_obs_loaded || !batch.TxnBegin ()) {
730- m_pending_obs_dirty = true ;
731- return false ;
732- }
733- // Write the locks of all entries, not only of the ones just added: entries added
734- // while nothing could be persisted are locked in memory only. A lock released
735- // manually in the meantime is left alone, the next check drops its entry.
736- bool fPersisted {batch.WriteCoinJoinPendingObs (m_pending_obs)};
737- for (auto it = m_pending_obs.begin (); fPersisted && it != m_pending_obs.end (); ++it) {
738- if (m_wallet->IsLockedCoin (it->first )) fPersisted = m_wallet->LockCoin (it->first , &batch);
739- }
740- if (fPersisted ) {
741- fPersisted = batch.TxnCommit ();
742- } else {
743- batch.TxnAbort ();
744- }
745- m_pending_obs_dirty = !fPersisted ;
746- return fPersisted ;
747- }
748-
749741void CCoinJoinClientManager::LoadPendingObservations (wallet::WalletBatch& batch)
750742{
751743 AssertLockHeld (cs_pending_obs);
@@ -798,6 +790,9 @@ void CCoinJoinClientManager::CheckPendingObservations(const CTxMemPool& mempool)
798790 LOCK (m_wallet->cs_wallet );
799791 LOCK (cs_pending_obs);
800792
793+ // Entries added while the record could not be read are locked in memory only (see
794+ // AddPendingObservation()), persist them once the record has been recovered
795+ bool fChanged {!m_pending_obs_loaded && !m_pending_obs.empty ()};
801796 if (!m_pending_obs_loaded) {
802797 // Read-only, no need to checkpoint the database on the way out
803798 wallet::WalletBatch batch_load (m_wallet->GetDatabase (), /* _fFlushOnClose=*/ false );
@@ -815,7 +810,6 @@ void CCoinJoinClientManager::CheckPendingObservations(const CTxMemPool& mempool)
815810
816811 const int64_t nNow{GetTime ()};
817812 const bool fSynced {m_mn_sync.IsBlockchainSynced ()};
818- bool fChanged {false };
819813 for (auto it = m_pending_obs.begin (); it != m_pending_obs.end ();) {
820814 const COutPoint& outpoint = it->first ;
821815 if (!m_wallet->IsLockedCoin (outpoint)) {
@@ -877,16 +871,7 @@ void CCoinJoinClientManager::CheckPendingObservations(const CTxMemPool& mempool)
877871 // it may still track locks nothing else would ever release. An entry released
878872 // above but left in such a record self-heals: once the record is readable again
879873 // the entry reloads, its coin is no longer locked and it is dropped right here.
880- if (!m_pending_obs_loaded) return ;
881- bool fPersisted {true };
882- if (m_pending_obs_dirty) {
883- // Some entries could not be persisted when they were added (e.g. the record could
884- // not be read back then) and are only locked in memory, persist their locks too
885- fPersisted = PersistPendingObservations (get_batch ());
886- } else if (fChanged ) {
887- fPersisted = get_batch ().WriteCoinJoinPendingObs (m_pending_obs);
888- }
889- if (!fPersisted ) {
874+ if (fChanged && m_pending_obs_loaded && !PersistPendingObservations (*m_wallet, get_batch (), m_pending_obs)) {
890875 LogPrintf (" CCoinJoinClientManager::%s -- ERROR: failed to persist %d pending observation(s)\n " , __func__,
891876 m_pending_obs.size ());
892877 }
0 commit comments