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.Where(x => x.TransactionId == 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.TryRemove(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.TryRemove(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.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).Sum(x => x.Amount); 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> 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.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).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)) { CoinReceived?.Invoke(this, 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.FirstOrDefault(x => x.TransactionId == txId && x.Index == 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.FirstOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N); if (foundCoin != null) { walletRelevant = true; var alreadyKnown = foundCoin.SpenderTransactionId == txId; foundCoin.SpenderTransactionId = txId; if (!alreadyKnown) { CoinSpent?.Invoke(this, foundCoin); } if (tx.Confirmed) { SpenderConfirmed?.Invoke(this, foundCoin); } } } if (walletRelevant) { TransactionStore.AddOrUpdate(tx); } return(walletRelevant); }
private ProcessedResult ProcessNoLock(SmartTransaction tx) { var result = new ProcessedResult(tx); // We do not care about non-witness transactions for other than mempool cleanup. if (tx.Transaction.PossiblyP2WPKHInvolved()) { uint256 txId = tx.GetHash(); // Performance ToDo: txids could be cached in a hashset here by the AllCoinsView and then the contains would be fast. if (!tx.Transaction.IsCoinBase && !Coins.AsAllCoinsView().CreatedBy(txId).Any()) // Transactions we already have and processed would be "double spends" but they shouldn't. { var doubleSpends = new List <SmartCoin>(); foreach (SmartCoin coin in Coins.AsAllCoinsView()) { 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; 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. var isReplacemenetTx = doubleSpends.Any(x => x.IsReplaceable && !x.Confirmed); if (isReplacemenetTx) { // Undo the replaced transaction by removing the coins it created (if other coin // spends it, remove that too and so on) and restoring those that it replaced. // After undoing the replaced transaction it will process the replacement transaction. var replacedTxId = doubleSpends.First().TransactionId; var(replaced, restored) = Coins.Undo(replacedTxId); result.ReplacedCoins.AddRange(replaced); result.RestoredCoins.AddRange(restored); foreach (var replacedTransactionId in replaced.Select(coin => coin.TransactionId)) { TransactionStore.MempoolStore.TryRemove(replacedTransactionId, out _); } tx.SetReplacement(); } else { return(result); } } 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); } result.SuccessfullyDoubleSpentCoins.AddRange(doubleSpends); var unconfirmedDoubleSpentTxId = doubleSpends.First().TransactionId; TransactionStore.MempoolStore.TryRemove(unconfirmedDoubleSpentTxId, out _); } } } bool hasEqualOutputs = tx.Transaction.HasIndistinguishableOutputs(); 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.AsAllCoinsView().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) { result.IsLikelyOwnCoinJoin = true; } } } 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) { if (output.Value <= DustThreshold) { result.ReceivedDusts.Add(output); 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, result.IsLikelyOwnCoinJoin, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey); // Do not inherit locked status from key, that's different. result.ReceivedCoins.Add(newCoin); // If we did not have it. if (Coins.TryAdd(newCoin)) { result.NewlyReceivedCoins.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 is { }) // Just to be sure, it is a concurrent collection.
private Dictionary <int, WitScript> SignCoinJoin(CcjClientRound ongoingRound, Transaction unsignedCoinJoin) { TxOut[] myOutputs = unsignedCoinJoin.Outputs .Where(x => x.ScriptPubKey == ongoingRound.Registration.ChangeAddress.ScriptPubKey || ongoingRound.Registration.ActiveOutputs.Select(y => y.Address.ScriptPubKey).Contains(x.ScriptPubKey)) .ToArray(); Money amountBack = myOutputs.Sum(y => y.Value); // Make sure change is counted. Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.Amount); // Start with input sum. // Do outputs.lenght + 1 in case the server estimated the network fees wrongly due to insufficient data in an edge case. Money networkFeesAfterOutputs = ongoingRound.State.FeePerOutputs * (ongoingRound.Registration.AliceClient.RegisteredAddresses.Length + 1); // Use registered addresses here, because network fees are decided at inputregistration. Money networkFeesAfterInputs = ongoingRound.State.FeePerInputs * ongoingRound.Registration.CoinsRegistered.Count(); Money networkFees = networkFeesAfterOutputs + networkFeesAfterInputs; minAmountBack -= networkFees; // Minus miner fee. IOrderedEnumerable <(Money value, int count)> indistinguishableOutputs = unsignedCoinJoin.GetIndistinguishableOutputs(includeSingle: false).OrderByDescending(x => x.count); foreach ((Money value, int count)denomPair in indistinguishableOutputs) { var mineCount = myOutputs.Count(x => x.Value == denomPair.value); Money denomination = denomPair.value; int anonset = Math.Min(110, denomPair.count); // https://github.com/zkSNACKs/WalletWasabi/issues/1379 Money expectedCoordinatorFee = denomination.Percentage(ongoingRound.State.CoordinatorFeePercent * anonset); for (int i = 0; i < mineCount; i++) { minAmountBack -= expectedCoordinatorFee; // Minus expected coordinator fee. } } // If there's no change output then coordinator protection may happened: bool gotChange = myOutputs.Select(x => x.ScriptPubKey).Contains(ongoingRound.Registration.ChangeAddress.ScriptPubKey); if (!gotChange) { Money minimumOutputAmount = Money.Coins(0.0001m); // If the change would be less than about $1 then add it to the coordinator. Money baseDenomination = indistinguishableOutputs.First().value; Money onePercentOfDenomination = baseDenomination.Percentage(1m); // If the change is less than about 1% of the newDenomination then add it to the coordinator fee. Money minimumChangeAmount = Math.Max(minimumOutputAmount, onePercentOfDenomination); minAmountBack -= minimumChangeAmount; // Minus coordinator protections (so it won't create bad coinjoins.) } if (amountBack < minAmountBack && !amountBack.Almost(minAmountBack, Money.Satoshis(1000))) // Just in case. Rounding error maybe? { Money diff = minAmountBack - amountBack; throw new NotSupportedException($"Coordinator did not add enough value to our outputs in the coinjoin. Missing: {diff.Satoshi} satoshis."); } var signedCoinJoin = unsignedCoinJoin.Clone(); signedCoinJoin.Sign(ongoingRound.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(SaltSoup(), x.ScriptPubKey).Single()).ToArray(), ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin()).ToArray()); // Old way of signing, which randomly fails! https://github.com/zkSNACKs/WalletWasabi/issues/716#issuecomment-435498906 // Must be fixed in NBitcoin. //var builder = Network.CreateTransactionBuilder(); //var signedCoinJoin = builder // .ContinueToBuild(unsignedCoinJoin) // .AddKeys(ongoingRound.Registration.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(OnePiece, x.ScriptPubKey).Single()).ToArray()) // .AddCoins(ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin())) // .BuildTransaction(true); var myDic = new Dictionary <int, WitScript>(); for (int i = 0; i < signedCoinJoin.Inputs.Count; i++) { var input = signedCoinJoin.Inputs[i]; if (ongoingRound.CoinsRegistered.Select(x => x.GetOutPoint()).Contains(input.PrevOut)) { myDic.Add(i, signedCoinJoin.Inputs[i].WitScript); } } return(myDic); }
private void WalletManager_WalletRelevantTransactionProcessed(object sender, ProcessedResult e) { try { // If there are no news, then don't bother. if (!e.IsNews || (sender as Wallet).State != WalletState.Started) { return; } // ToDo // Double spent. // Anonymity set gained? // Received dust bool isSpent = e.NewlySpentCoins.Any(); bool isReceived = e.NewlyReceivedCoins.Any(); bool isConfirmedReceive = e.NewlyConfirmedReceivedCoins.Any(); bool isConfirmedSpent = e.NewlyConfirmedReceivedCoins.Any(); Money miningFee = e.Transaction.Transaction.GetFee(e.SpentCoins.Select(x => x.GetCoin()).ToArray()); if (isReceived || isSpent) { Money receivedSum = e.NewlyReceivedCoins.Sum(x => x.Amount); Money spentSum = e.NewlySpentCoins.Sum(x => x.Amount); Money incoming = receivedSum - spentSum; Money receiveSpentDiff = incoming.Abs(); string amountString = receiveSpentDiff.ToString(false, true); if (e.Transaction.Transaction.IsCoinBase) { _notificationManager.NotifyAndLog($"{amountString} BTC", "Mined", NotificationType.Success, e); } else if (isSpent && receiveSpentDiff == miningFee) { _notificationManager.NotifyAndLog($"Mining Fee: {amountString} BTC", "Self Spend", NotificationType.Information, e); } else if (isSpent && receiveSpentDiff.Almost(Money.Zero, Money.Coins(0.01m)) && e.IsLikelyOwnCoinJoin) { _notificationManager.NotifyAndLog($"CoinJoin Completed!", "", NotificationType.Success, e); } else if (incoming > Money.Zero) { if (e.Transaction.IsRBF && e.Transaction.IsReplacement) { _notificationManager.NotifyAndLog($"{amountString} BTC", "Received Replaceable Replacement Transaction", NotificationType.Information, e); } else if (e.Transaction.IsRBF) { _notificationManager.NotifyAndLog($"{amountString} BTC", "Received Replaceable Transaction", NotificationType.Success, e); } else if (e.Transaction.IsReplacement) { _notificationManager.NotifyAndLog($"{amountString} BTC", "Received Replacement Transaction", NotificationType.Information, e); } else { _notificationManager.NotifyAndLog($"{amountString} BTC", "Received", NotificationType.Success, e); } } else if (incoming < Money.Zero) { _notificationManager.NotifyAndLog($"{amountString} BTC", "Sent", NotificationType.Information, e); } } else if (isConfirmedReceive || isConfirmedSpent) { Money receivedSum = e.ReceivedCoins.Sum(x => x.Amount); Money spentSum = e.SpentCoins.Sum(x => x.Amount); Money incoming = receivedSum - spentSum; Money receiveSpentDiff = incoming.Abs(); string amountString = receiveSpentDiff.ToString(false, true); if (isConfirmedSpent && receiveSpentDiff == miningFee) { _notificationManager.NotifyAndLog($"Mining Fee: {amountString} BTC", "Self Spend Confirmed", NotificationType.Information, e); } else if (isConfirmedSpent && e.IsLikelyOwnCoinJoin) { _notificationManager.NotifyAndLog($"CoinJoin Confirmed!", "", NotificationType.Information, e); } else if (incoming > Money.Zero) { _notificationManager.NotifyAndLog($"{amountString} BTC", "Receive Confirmed", NotificationType.Information, e); } else if (incoming < Money.Zero) { _notificationManager.NotifyAndLog($"{amountString} BTC", "Send Confirmed", NotificationType.Information, e); } } } catch (Exception ex) { Logger.LogWarning(ex); } }