private bool TryUpdateNoLockNoSerialization(SmartTransaction tx) { var hash = tx.GetHash(); if (Transactions.TryGetValue(hash, out SmartTransaction found)) { return(found.TryUpdate(tx)); } return(false); }
public bool TryGetTransaction(uint256 hash, out SmartTransaction sameStx) { lock (Lock) { if (MempoolStore.TryGetTransaction(hash, out sameStx)) { return(true); } return(ConfirmedStore.TryGetTransaction(hash, out sameStx)); } }
private bool TryAddNoLockNoSerialization(SmartTransaction tx) { var hash = tx.GetHash(); if (Transactions.TryAdd(hash, tx)) { return(true); } else { Transactions[hash].Update(tx); return(false); } }
public bool TryRemove(uint256 hash, out SmartTransaction stx) { bool isRemoved; lock (TransactionsLock) { isRemoved = Transactions.Remove(hash, out stx); } if (isRemoved) { _ = TryRemoveFromFileAsync(hash); } return(isRemoved); }
public bool TryUpdate(SmartTransaction tx) { bool ret; lock (TransactionsLock) { ret = TryUpdateNoLockNoSerialization(tx); } if (ret) { _ = TryUpdateFileAsync(tx); } return(ret); }
public bool TryAdd(SmartTransaction tx) { bool isAdded; lock (TransactionsLock) { isAdded = TryAddNoLockNoSerialization(tx); } if (isAdded) { _ = TryAppendToFileAsync(tx); } return(isAdded); }
private async Task InitializeTransactionsNoMutexAsync() { try { IoHelpers.EnsureFileExists(TransactionsFileManager.FilePath); var allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false); var allTransactions = allLines .Select(x => SmartTransaction.FromLine(x, Network)) .OrderByBlockchain(); var added = false; var updated = false; lock (TransactionsLock) { foreach (var tx in allTransactions) { var res = TryAddOrUpdateNoLockNoSerialization(tx); if (res.isAdded) { added = true; } if (res.isUpdated) { updated = true; } } } if (added || updated) { // Another process worked into the file and appended the same transaction into it. // In this case we correct the file by serializing the unique set. await SerializeAllTransactionsNoMutexAsync().ConfigureAwait(false); } } catch { // We found a corrupted entry. Stop here. // Delete the currupted file. // Do not try to autocorrect, because the internal data structures are throwing events that may confuse the consumers of those events. Logger.LogError($"{TransactionsFileManager.FileNameWithoutExtension} file got corrupted. Deleting it..."); TransactionsFileManager.DeleteMe(); throw; } }
private (bool isAdded, bool isUpdated) TryAddOrUpdateNoLockNoSerialization(SmartTransaction tx) { var hash = tx.GetHash(); if (Transactions.TryAdd(hash, tx)) { return(true, false); } else { if (Transactions[hash].TryUpdate(tx)) { return(false, true); } else { return(false, false); } } }
public (bool isAdded, bool isUpdated) TryAddOrUpdate(SmartTransaction tx) { (bool isAdded, bool isUpdated)ret; lock (TransactionsLock) { ret = TryAddOrUpdateNoLockNoSerialization(tx); } if (ret.isAdded) { _ = TryAppendToFileAsync(tx); } if (ret.isUpdated) { _ = TryUpdateFileAsync(tx); } return(ret); }
private void AddOrUpdateNoLock(SmartTransaction tx) { var hash = tx.GetHash(); if (tx.Confirmed) { if (MempoolStore.TryRemove(hash, out SmartTransaction found)) { found.TryUpdate(tx); ConfirmedStore.TryAddOrUpdate(found); } else { ConfirmedStore.TryAddOrUpdate(tx); } } else { if (!ConfirmedStore.TryUpdate(tx)) { MempoolStore.TryAddOrUpdate(tx); } } }
private async Task TryCommitToFileAsync(ITxStoreOperation operation) { try { if (operation is null || operation.IsEmpty) { return; } // Make sure that only one call can continue. lock (OperationsLock) { var isRunning = Operations.Any(); Operations.Add(operation); if (isRunning) { return; } } // Wait until the operation list calms down. IEnumerable <ITxStoreOperation> operationsToExecute; while (true) { var count = Operations.Count; await Task.Delay(100).ConfigureAwait(false); lock (OperationsLock) { if (count == Operations.Count) { // Merge operations. operationsToExecute = OperationMerger.Merge(Operations).ToList(); Operations.Clear(); break; } } } using (await TransactionsFileManager.Mutex.LockAsync().ConfigureAwait(false)) { foreach (ITxStoreOperation op in operationsToExecute) { if (op is Append appendOperation) { var toAppends = appendOperation.Transactions; try { await TransactionsFileManager.AppendAllLinesAsync(toAppends.ToBlockchainOrderedLines()).ConfigureAwait(false); } catch { await SerializeAllTransactionsNoMutexAsync().ConfigureAwait(false); } } else if (op is Remove removeOperation) { var toRemoves = removeOperation.Transactions; string[] allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false); var toSerialize = new List <string>(); foreach (var line in allLines) { var startsWith = false; foreach (var toRemoveString in toRemoves.Select(x => x.ToString())) { startsWith = startsWith || line.StartsWith(toRemoveString, StringComparison.Ordinal); } if (!startsWith) { toSerialize.Add(line); } } try { await TransactionsFileManager.WriteAllLinesAsync(toSerialize).ConfigureAwait(false); } catch { await SerializeAllTransactionsNoMutexAsync().ConfigureAwait(false); } } else if (op is Update updateOperation) { var toUpdates = updateOperation.Transactions; string[] allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false); IEnumerable <SmartTransaction> allTransactions = allLines.Select(x => SmartTransaction.FromLine(x, Network)); var toSerialize = new List <SmartTransaction>(); foreach (SmartTransaction tx in allTransactions) { var txsToUpdateWith = toUpdates.Where(x => x == tx); foreach (var txToUpdateWith in txsToUpdateWith) { tx.TryUpdate(txToUpdateWith); } toSerialize.Add(tx); } try { await TransactionsFileManager.WriteAllLinesAsync(toSerialize.ToBlockchainOrderedLines()).ConfigureAwait(false); } catch { await SerializeAllTransactionsNoMutexAsync().ConfigureAwait(false); } } else { throw new NotSupportedException(); } } } } catch (Exception ex) { Logger.LogError(ex); } }
public bool Process(SmartTransaction tx) { if (!tx.Transaction.PossiblyP2WPKHInvolved()) { return(false); // We do not care about non-witness transactions for other than mempool cleanup. } uint256 txId = tx.GetHash(); var walletRelevant = false; if (tx.Confirmed) { foreach (var coin in Coins.AsAllCoinsView().CreatedBy(txId)) { coin.Height = tx.Height; walletRelevant = true; // relevant } } if (!tx.Transaction.IsCoinBase) // Transactions we already have and processed would be "double spends" but they shouldn't. { var doubleSpends = new List <SmartCoin>(); foreach (SmartCoin coin in Coins) { var spent = false; foreach (TxoRef spentOutput in coin.SpentOutputs) { foreach (TxIn txIn in tx.Transaction.Inputs) { if (spentOutput.TransactionId == txIn.PrevOut.Hash && spentOutput.Index == txIn.PrevOut.N) // Do not do (spentOutput == txIn.PrevOut), it's faster this way, because it won't check for null. { doubleSpends.Add(coin); spent = true; walletRelevant = true; break; } } if (spent) { break; } } } if (doubleSpends.Any()) { if (tx.Height == Height.Mempool) { // if the received transaction is spending at least one input already // spent by a previous unconfirmed transaction signaling RBF then it is not a double // spanding transaction but a replacement transaction. if (doubleSpends.Any(x => x.IsReplaceable)) { // remove double spent coins (if other coin spends it, remove that too and so on) // will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends.Where(x => !x.Confirmed)) { Coins.Remove(doubleSpentCoin); } tx.SetReplacement(); walletRelevant = true; } else { return(false); } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends) { Coins.Remove(doubleSpentCoin); } walletRelevant = true; } } } var isLikelyCoinJoinOutput = false; bool hasEqualOutputs = tx.Transaction.GetIndistinguishableOutputs(includeSingle: false).FirstOrDefault() != default; if (hasEqualOutputs) { var receiveKeys = KeyManager.GetKeys(x => tx.Transaction.Outputs.Any(y => y.ScriptPubKey == x.P2wpkhScript)); bool allReceivedInternal = receiveKeys.All(x => x.IsInternal); if (allReceivedInternal) { // It is likely a coinjoin if the diff between receive and sent amount is small and have at least 2 equal outputs. Money spentAmount = Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).TotalAmount(); Money receivedAmount = tx.Transaction.Outputs.Where(x => receiveKeys.Any(y => y.P2wpkhScript == x.ScriptPubKey)).Sum(x => x.Value); bool receivedAlmostAsMuchAsSpent = spentAmount.Almost(receivedAmount, Money.Coins(0.005m)); if (receivedAlmostAsMuchAsSpent) { isLikelyCoinJoinOutput = true; } } } List <SmartCoin> newCoins = new List <SmartCoin>(); List <SmartCoin> spentOwnCoins = null; for (var i = 0U; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey); if (foundKey != default) { walletRelevant = true; if (output.Value <= DustThreshold) { continue; } foundKey.SetKeyState(KeyState.Used, KeyManager); spentOwnCoins ??= Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).ToList(); var anonset = tx.Transaction.GetAnonymitySet(i); if (spentOwnCoins.Count != 0) { anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1; // Minus 1, because do not count own. } SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, isLikelyCoinJoinOutput, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey); // Do not inherit locked status from key, that's different. // If we did not have it. if (Coins.TryAdd(newCoin)) { newCoins.Add(newCoin); // Make sure there's always 21 clean keys generated and indexed. KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal); if (foundKey.IsInternal) { // Make sure there's always 14 internal locked keys generated and indexed. KeyManager.AssertLockedInternalKeysIndexed(14); } } else // If we had this coin already. { if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had. { SmartCoin oldCoin = Coins.AsAllCoinsView().GetByOutPoint(new OutPoint(txId, i)); if (oldCoin != null) // Just to be sure, it is a concurrent collection. { oldCoin.Height = newCoin.Height; } } } } } // If spends any of our coin for (var i = 0; i < tx.Transaction.Inputs.Count; i++) { var input = tx.Transaction.Inputs[i]; var foundCoin = Coins.GetByOutPoint(input.PrevOut); if (foundCoin != null) { walletRelevant = true; var alreadyKnown = foundCoin.SpenderTransactionId == txId; foundCoin.SpenderTransactionId = txId; if (!alreadyKnown) { Coins.Spend(foundCoin); CoinSpent?.Invoke(this, foundCoin); } if (tx.Confirmed) { SpenderConfirmed?.Invoke(this, foundCoin); } } } foreach (var newCoin in newCoins) { CoinReceived?.Invoke(this, newCoin); } if (walletRelevant) { TransactionStore.AddOrUpdate(tx); } return(walletRelevant); }