Esempio n. 1
0
        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);
        }
Esempio n. 2
0
        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.
Esempio n. 3
0
        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);
            }
        }