Esempio n. 1
0
        private void ProcessTransaction(SmartTransaction tx, List <HdPubKey> keys = null)
        {
            // If key list is not provided refresh the key list.
            if (keys == null)
            {
                keys = KeyManager.GetKeys().ToList();
            }

            // If transaction received to any of the wallet keys:
            for (var i = 0; i < tx.Transaction.Outputs.Count; i++)
            {
                var      output   = tx.Transaction.Outputs[i];
                HdPubKey foundKey = keys.SingleOrDefault(x => x.GetP2wpkhScript() == output.ScriptPubKey);
                if (foundKey != default)
                {
                    // If we already had it, just update the height. Maybe got from mempool to block or reorged.
                    var foundCoin = Coins.SingleOrDefault(x => x.TransactionId == tx.GetHash() && x.Index == i);
                    if (foundCoin != default)
                    {
                        // If tx height is mempool then don't, otherwise update the height.
                        if (tx.Height == Height.MemPool)
                        {
                            continue;
                        }
                        else
                        {
                            foundCoin.Height = tx.Height;
                            continue;
                        }
                    }

                    foundKey.KeyState = KeyState.Used;
                    var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, foundKey.Label, null);
                    Coins.Add(coin);

                    // Make sure there's always 21 clean keys generated and indexed.
                    if (AssertCleanKeysIndexed(21, foundKey.IsInternal()))
                    {
                        // If it generated a new key refresh the keys:
                        keys = KeyManager.GetKeys().ToList();
                    }
                }
            }

            // 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.SingleOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N);
                if (foundCoin != null)
                {
                    foundCoin.SpenderTransactionId = tx.GetHash();
                }
            }
        }
        private async Task BroadcastTransactionToNetworkNodeAsync(SmartTransaction transaction, Node node)
        {
            Logger.LogInfo($"Trying to broadcast transaction with random node ({node.RemoteSocketAddress}):{transaction.GetHash()}.");
            if (!BitcoinStore.MempoolService.TryAddToBroadcastStore(transaction.Transaction, node.RemoteSocketEndpoint.ToString()))             // So we'll reply to INV with this transaction.
            {
                Logger.LogWarning($"Transaction {transaction.GetHash()} was already present in the broadcast store.");
            }
            var invPayload = new InvPayload(transaction.Transaction);

            // Give 7 seconds to send the inv payload.
            await node.SendMessageAsync(invPayload).WithAwaitCancellationAsync(TimeSpan.FromSeconds(7)).ConfigureAwait(false);             // ToDo: It's dangerous way to cancel. Implement proper cancellation to NBitcoin!

            if (BitcoinStore.MempoolService.TryGetFromBroadcastStore(transaction.GetHash(), out TransactionBroadcastEntry? entry))
            {
                // Give 7 seconds for serving.
                var timeout = 0;
                while (!entry.IsBroadcasted())
                {
                    if (timeout > 7)
                    {
                        throw new TimeoutException("Did not serve the transaction.");
                    }
                    await Task.Delay(1_000).ConfigureAwait(false);

                    timeout++;
                }
                node.DisconnectAsync("Thank you!");
                Logger.LogInfo($"Disconnected node: {node.RemoteSocketAddress}. Successfully broadcasted transaction: {transaction.GetHash()}.");

                // Give 21 seconds for propagation.
                timeout = 0;
                while (entry.GetPropagationConfirmations() < 2)
                {
                    if (timeout > 21)
                    {
                        throw new TimeoutException("Did not serve the transaction.");
                    }
                    await Task.Delay(1_000).ConfigureAwait(false);

                    timeout++;
                }
                Logger.LogInfo($"Transaction is successfully propagated: {transaction.GetHash()}.");
            }
            else
            {
                Logger.LogWarning($"Expected transaction {transaction.GetHash()} was not found in the broadcast store.");
            }
        }
        private async Task BroadcastTransactionToBackendAsync(SmartTransaction transaction)
        {
            Logger.LogInfo("Broadcasting with backend...");
            using (var client = new WasabiClient(Synchronizer.WasabiClient.TorClient.DestinationUriAction, Synchronizer.WasabiClient.TorClient.TorSocks5EndPoint))
            {
                try
                {
                    await client.BroadcastAsync(transaction).ConfigureAwait(false);
                }
                catch (HttpRequestException ex2) when(ex2.Message.Contains("bad-txns-inputs-missingorspent", StringComparison.InvariantCultureIgnoreCase) ||
                                                      ex2.Message.Contains("missing-inputs", StringComparison.InvariantCultureIgnoreCase) ||
                                                      ex2.Message.Contains("txn-mempool-conflict", StringComparison.InvariantCultureIgnoreCase))
                {
                    if (transaction.Transaction.Inputs.Count == 1)                     // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know.
                    {
                        OutPoint input = transaction.Transaction.Inputs.First().PrevOut;
                        lock (WalletServicesLock)
                        {
                            foreach (var walletService in AliveWalletServices)
                            {
                                SmartCoin coin = walletService.Coins.GetByOutPoint(input);
                                if (coin != default)
                                {
                                    coin.SpentAccordingToBackend = true;
                                }
                            }
                        }
                    }
                }
            }

            BelieveTransaction(transaction);

            Logger.LogInfo($"Transaction is successfully broadcasted to backend: {transaction.GetHash()}.");
        }
        private async Task BroadcastTransactionWithRpcAsync(SmartTransaction transaction)
        {
            await RpcClient.SendRawTransactionAsync(transaction.Transaction).ConfigureAwait(false);

            BelieveTransaction(transaction);
            Logger.LogInfo($"Transaction is successfully broadcasted with RPC: {transaction.GetHash()}.");
        }
Esempio n. 5
0
        public async Task ConfirmTransactionTestAsync()
        {
            var transactionProcessor = await CreateTransactionProcessorAsync();

            var keys          = transactionProcessor.KeyManager.GetKeys().ToArray();
            int coinsReceived = 0;
            int confirmed     = 0;

            transactionProcessor.CoinReceived     += (s, e) => coinsReceived++;
            transactionProcessor.CoinSpent        += (s, e) => throw new InvalidOperationException("We are not spending the coin.");
            transactionProcessor.SpenderConfirmed += (s, e) => confirmed++;

            // A confirmed segwit transaction for us
            var tx = CreateCreditingTransaction(keys[0].PubKey.WitHash.ScriptPubKey, Money.Coins(1.0m));

            transactionProcessor.Process(tx);
            Assert.Equal(1, coinsReceived);

            var coin = Assert.Single(transactionProcessor.Coins);

            Assert.False(coin.Confirmed);

            var tx2 = new SmartTransaction(tx.Transaction.Clone(), new Height(54321));

            Assert.Equal(tx.GetHash(), tx2.GetHash());
            transactionProcessor.Process(tx2);
            Assert.Equal(1, coinsReceived);
            Assert.True(coin.Confirmed);

            Assert.Equal(0, confirmed);
        }
    public bool TryUpdate(SmartTransaction tx)
    {
        var hash = tx.GetHash();

        lock (Lock)
        {
            // Do Contains first, because it's fast.
            if (ConfirmedStore.TryUpdate(tx))
            {
                return(true);
            }
            else if (tx.Confirmed && MempoolStore.TryRemove(hash, out var originalTx))
            {
                originalTx.TryUpdate(tx);
                ConfirmedStore.TryAddOrUpdate(originalTx);
                return(true);
            }
            else if (MempoolStore.TryUpdate(tx))
            {
                return(true);
            }
        }

        return(false);
    }
        private async Task BroadcastTransactionToBackendAsync(SmartTransaction transaction)
        {
            Logger.LogInfo("Broadcasting with backend...");
            using (TorHttpClient torHttpClient = Synchronizer.HttpClientFactory.NewBackendTorHttpClient(isolateStream: true))
            {
                var client = new WasabiClient(torHttpClient);

                try
                {
                    await client.BroadcastAsync(transaction).ConfigureAwait(false);
                }
                catch (HttpRequestException ex2) when(RpcErrorTools.IsSpentError(ex2.Message))
                {
                    if (transaction.Transaction.Inputs.Count == 1)                     // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know.
                    {
                        OutPoint input = transaction.Transaction.Inputs.First().PrevOut;
                        foreach (var coin in WalletManager.CoinsByOutPoint(input))
                        {
                            coin.SpentAccordingToBackend = true;
                        }
                    }

                    throw;
                }
            }

            BelieveTransaction(transaction);

            Logger.LogInfo($"Transaction is successfully broadcasted to backend: {transaction.GetHash()}.");
        }
        private void AnalyzeCoinjoin(SmartTransaction tx, int newInputAnonset, ISet <HdPubKey> distinctWalletInputPubKeys)
        {
            var indistinguishableWalletOutputs = tx
                                                 .WalletOutputs.GroupBy(x => x.Amount)
                                                 .ToDictionary(x => x.Key, y => y.Count());

            var anonsets = tx.Transaction.GetAnonymitySets(tx.WalletOutputs.Select(x => x.Index));

            foreach (var newCoin in tx.WalletOutputs.ToArray())
            {
                // Begin estimating the anonymity set size based on the number of
                // equivalent outputs that the i-th output has in in the transaction.
                int anonset = anonsets[newCoin.Index];

                // Picking randomly an output would make our anonset: total/ours.
                anonset /= indistinguishableWalletOutputs[newCoin.Amount];

                // Account for the inherited anonymity set size from the inputs in the
                // anonymity set size estimate.
                // The anonymity set size estimated for the input cluster is corrected
                // by -1 to avoid double counting ourselves in the anonset.
                // Stated differently, the right value to use for the calculation is not the
                // anonymity set size, but the subset of only the other participants, since
                // every output must belong to a member of the set.
                anonset += newInputAnonset - 1;

                HdPubKey hdPubKey = newCoin.HdPubKey;
                uint256  txid     = tx.GetHash();
                if (hdPubKey.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)
                {
                    // If the new coin's HD pubkey haven't been used yet
                    // then its anonset haven't been set yet.
                    // In that case the acquired anonset does not have to be intersected with the default anonset,
                    // so this coin gets the aquired anonset.
                    hdPubKey.SetAnonymitySet(anonset, txid);
                }
                else if (distinctWalletInputPubKeys.Contains(hdPubKey))
                {
                    // If it's a reuse of an input's pubkey, then intersection punishment is senseless.
                    hdPubKey.SetAnonymitySet(newInputAnonset, txid);
                }
                else if (tx.WalletOutputs.Where(x => x != newCoin).Select(x => x.HdPubKey).Contains(hdPubKey))
                {
                    // If it's a reuse of another output' pubkey, then intersection punishment can only go as low as the inherited anonset.
                    hdPubKey.SetAnonymitySet(Math.Max(newInputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1)), txid);
                }
                else if (hdPubKey.AnonymitySetReasons.Contains(txid))
                {
                    // If we already processed this transaction for this script
                    // then we'll go with normal processing, it's not an address reuse,
                    // it's just we're processing the transaction twice.
                    hdPubKey.SetAnonymitySet(anonset, txid);
                }
                else
                {
                    // It's address reuse.
                    hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1), txid);
                }
            }
        }
Esempio n. 9
0
        private void OnNext()
        {
            Navigate().Clear();

            var walletViewModel = UiServices.WalletManager.GetWalletViewModel(_wallet);

            walletViewModel.History.SelectTransaction(_finalTransaction.GetHash());
        }
 private static void AnalyzeReceive(SmartTransaction tx)
 {
     // No matter how much anonymity a user would had gained in a tx, if the money comes from outside, then make anonset 1.
     foreach (var key in tx.WalletOutputs.Select(x => x.HdPubKey))
     {
         key.SetAnonymitySet(1, tx.GetHash());
     }
 }
Esempio n. 11
0
 public TransactionBroadcastEntry(SmartTransaction transaction, string nodeRemoteSocketEndpoint)
 {
     Lock                     = new object();
     Transaction              = transaction;
     TransactionId            = Transaction.GetHash();
     Broadcasted              = false;
     PropagationConfirmations = 0;
     NodeRemoteSocketEndpoint = nodeRemoteSocketEndpoint;
 }
        public async Task SendTransactionAsync(SmartTransaction transaction)
        {
            try
            {
                // Broadcast to a random node.
                // Wait until it arrives to at least two other nodes.
                // If something's wrong, fall back broadcasting with rpc, then backend.

                if (Network == Network.RegTest)
                {
                    throw new InvalidOperationException($"Transaction broadcasting to nodes does not work in {Network.RegTest}.");
                }

                Node node = Nodes.ConnectedNodes.RandomElement();
                while (node == default(Node) || !node.IsConnected || Nodes.ConnectedNodes.Count < 5)
                {
                    // As long as we are connected to at least 4 nodes, we can always try again.
                    // 3 should be enough, but make it 5 so 2 nodes could disconnect the meantime.
                    if (Nodes.ConnectedNodes.Count < 5)
                    {
                        throw new InvalidOperationException("We are not connected to enough nodes.");
                    }
                    await Task.Delay(100).ConfigureAwait(false);

                    node = Nodes.ConnectedNodes.RandomElement();
                }
                await BroadcastTransactionToNetworkNodeAsync(transaction, node).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Logger.LogInfo($"Random node could not broadcast transaction. Reason: {ex.Message}.");
                Logger.LogDebug(ex);

                if (RpcClient != null)
                {
                    try
                    {
                        await BroadcastTransactionWithRpcAsync(transaction).ConfigureAwait(false);
                    }
                    catch (Exception ex2)
                    {
                        Logger.LogInfo($"RPC could not broadcast transaction. Reason: {ex2.Message}.");
                        Logger.LogDebug(ex2);

                        await BroadcastTransactionToBackendAsync(transaction).ConfigureAwait(false);
                    }
                }
                else
                {
                    await BroadcastTransactionToBackendAsync(transaction).ConfigureAwait(false);
                }
            }
            finally
            {
                BitcoinStore.MempoolService.TryRemoveFromBroadcastStore(transaction.GetHash(), out _);                 // Remove it just to be sure. Probably has been removed previously.
            }
        }
Esempio n. 13
0
    private bool TryUpdateNoLockNoSerialization(SmartTransaction tx)
    {
        var hash = tx.GetHash();

        if (Transactions.TryGetValue(hash, out var found))
        {
            return(found.TryUpdate(tx));
        }

        return(false);
    }
 private static void AnalyzeNormalSpend(SmartTransaction tx)
 {
     // If all our inputs are ours and there's more than one output that isn't,
     // then we can assume that the persons the money was sent to learnt our inputs.
     // AND if there're outputs that go to someone else,
     // then we can assume that the people learnt our change outputs,
     // or at the very least assume that all the changes in the tx is ours.
     // For example even if the assumed change output is a payment to someone, a blockchain analyzer
     // probably would just assume it's ours and go on with its life.
     foreach (var key in tx.WalletInputs.Concat(tx.WalletOutputs).Select(x => x.HdPubKey))
     {
         key.SetAnonymitySet(1, tx.GetHash());
     }
 }
 private void AnalyzeSelfSpend(SmartTransaction tx, int newInputAnonset)
 {
     foreach (var key in tx.WalletOutputs.Select(x => x.HdPubKey))
     {
         if (key.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)
         {
             key.SetAnonymitySet(newInputAnonset, tx.GetHash());
         }
         else
         {
             key.SetAnonymitySet(Intersect(new[] { newInputAnonset, key.AnonymitySet }, 1), tx.GetHash());
         }
     }
 }
Esempio n. 16
0
 public bool TryAddToBroadcastStore(SmartTransaction transaction, string nodeRemoteSocketEndpoint)
 {
     lock (BroadcastStoreLock)
     {
         if (BroadcastStore.Any(x => x.TransactionId == transaction.GetHash()))
         {
             return(false);
         }
         else
         {
             var entry = new TransactionBroadcastEntry(transaction, nodeRemoteSocketEndpoint);
             BroadcastStore.Add(entry);
             return(true);
         }
     }
 }
        /// <summary>
        /// Adjusts the anonset of the inputs to the newly calculated output anonsets.
        /// </summary>
        private static void AdjustWalletInputs(SmartTransaction tx, HashSet <HdPubKey> distinctWalletInputPubKeys, int newInputAnonset)
        {
            // Sanity check.
            if (!tx.WalletOutputs.Any())
            {
                return;
            }

            var smallestOutputAnonset = tx.WalletOutputs.Min(x => x.HdPubKey.AnonymitySet);

            if (smallestOutputAnonset < newInputAnonset)
            {
                foreach (var key in distinctWalletInputPubKeys)
                {
                    key.SetAnonymitySet(smallestOutputAnonset, tx.GetHash());
                }
            }
        }
Esempio n. 18
0
        public SmartCoin(SmartTransaction transaction, uint outputIndex, HdPubKey pubKey)
        {
            Transaction    = transaction;
            Index          = outputIndex;
            _transactionId = new Lazy <uint256>(() => Transaction.GetHash(), true);

            _outPoint = new Lazy <OutPoint>(() => new OutPoint(TransactionId, Index), true);
            _txOut    = new Lazy <TxOut>(() => Transaction.Transaction.Outputs[Index], true);
            _coin     = new Lazy <Coin>(() => new Coin(OutPoint, TxOut), true);

            _hashCode = new Lazy <int>(() => OutPoint.GetHashCode(), true);

            Height = transaction.Height;

            HdPubKey = pubKey;

            Transaction.WalletOutputs.Add(this);
        }
        /// <param name="newInputAnonset">The new anonymity set of the inputs.</param>
        private void AnalyzeWalletInputs(SmartTransaction tx, out HashSet <HdPubKey> distinctWalletInputPubKeys, out int newInputAnonset)
        {
            // We want to weaken the punishment if the input merge happens in coinjoins.
            // Our strategy would be is to set the coefficient in proportion to our own inputs compared to the total inputs of the transaction.
            // However the accuracy can be increased if we consider every input with the same pubkey as a single input entity.
            // This we can only do for our own inputs as we don't know the pubkeys - nor the scripts - of other inputs.
            // Another way to think about this is: reusing pubkey on the input side is good, the punishment happened already.
            distinctWalletInputPubKeys = tx.WalletInputs.Select(x => x.HdPubKey).ToHashSet();
            var    distinctWalletInputPubKeyCount = distinctWalletInputPubKeys.Count;
            var    pubKeyReuseCount = tx.WalletInputs.Count - distinctWalletInputPubKeyCount;
            double coefficient      = (double)distinctWalletInputPubKeyCount / (tx.Transaction.Inputs.Count - pubKeyReuseCount);

            newInputAnonset = Intersect(distinctWalletInputPubKeys.Select(x => x.AnonymitySet), coefficient);

            foreach (var key in distinctWalletInputPubKeys)
            {
                key.SetAnonymitySet(newInputAnonset, tx.GetHash());
            }
        }
Esempio n. 20
0
    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);
            }
        }
    }
Esempio n. 21
0
    private async Task BroadcastTransactionToBackendAsync(SmartTransaction transaction)
    {
        Logger.LogInfo("Broadcasting with backend...");
        IHttpClient httpClient = HttpClientFactory.NewHttpClientWithCircuitPerRequest();

        WasabiClient client = new(httpClient);

        try
        {
            await client.BroadcastAsync(transaction).ConfigureAwait(false);
        }
        catch (HttpRequestException ex2) when(RpcErrorTools.IsSpentError(ex2.Message))
        {
            if (transaction.Transaction.Inputs.Count == 1)             // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know.
            {
                OutPoint input = transaction.Transaction.Inputs.First().PrevOut;
                foreach (var coin in WalletManager.CoinsByOutPoint(input))
                {
                    coin.SpentAccordingToBackend = true;
                }
            }

            // Exception message is in form: 'message:::tx1:::tx2:::etc.' where txs are encoded in HEX.
            IEnumerable <SmartTransaction> txs = ex2.Message.Split(":::", StringSplitOptions.RemoveEmptyEntries)
                                                 .Skip(1) // Skip the exception message.
                                                 .Select(x => new SmartTransaction(Transaction.Parse(x, Network), Height.Mempool));

            foreach (var tx in txs)
            {
                WalletManager.Process(tx);
            }

            throw;
        }

        BelieveTransaction(transaction);

        Logger.LogInfo($"Transaction is successfully broadcasted to backend: {transaction.GetHash()}.");
    }
    private void AddOrUpdateNoLock(SmartTransaction tx)
    {
        var hash = tx.GetHash();

        if (tx.Confirmed)
        {
            if (MempoolStore.TryRemove(hash, out var found))
            {
                found.TryUpdate(tx);
                ConfirmedStore.TryAddOrUpdate(found);
            }
            else
            {
                ConfirmedStore.TryAddOrUpdate(tx);
            }
        }
        else
        {
            if (!ConfirmedStore.TryUpdate(tx))
            {
                MempoolStore.TryAddOrUpdate(tx);
            }
        }
    }
        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 (var txin in tx.Transaction.Inputs)
                    {
                        if (Coins.TryGetSpenderSmartCoinsByOutPoint(txin.PrevOut, out var coins))
                        {
                            doubleSpends.AddRange(coins);
                        }
                    }

                    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
                            // spending transaction but a replacement transaction.
                            var isReplacementTx = doubleSpends.Any(x => x.IsReplaceable && !x.Confirmed);
                            if (isReplacementTx)
                            {
                                // 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 _);
                        }
                    }
                }

                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 is { })
Esempio n. 24
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. 25
0
        private void ProcessTransaction(SmartTransaction tx, List <HdPubKey> keys = null)
        {
            //iterate tx
            //	if already have the coin
            //		if NOT mempool
            //			update height

            //if double spend
            //	if mempool
            //		if all double spent coins are mempool and RBF
            //			remove double spent coins(if other coin spends it, remove that too and so on) // will add later if they came to our keys
            //		else
            //			return
            //	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

            //iterate tx
            //	if came to our keys
            //		add coin

            // If key list is not provided refresh the key list.
            if (keys == null)
            {
                keys = KeyManager.GetKeys().ToList();
            }

            for (var i = 0; i < tx.Transaction.Outputs.Count; i++)
            {
                // If we already had it, just update the height. Maybe got from mempool to block or reorged.
                var foundCoin = Coins.SingleOrDefault(x => x.TransactionId == tx.GetHash() && x.Index == i);
                if (foundCoin != default)
                {
                    // If tx height is mempool then don't, otherwise update the height.
                    if (tx.Height != Height.MemPool)
                    {
                        foundCoin.Height = tx.Height;
                    }
                }
            }

            // If double spend:
            IEnumerable <SmartCoin> doubleSpends = Coins.Where(x => tx.Transaction.Inputs.Any(y => x.SpentOutputs.Select(z => z.ToOutPoint()).Contains(y.PrevOut)));

            if (doubleSpends.Count() > 0)
            {
                if (tx.Height == Height.MemPool)
                {
                    // if all double spent coins are mempool and RBF
                    if (doubleSpends.All(x => x.Height == Height.MemPool && x.RBF))
                    {
                        // 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 (var doubleSpentCoin in doubleSpends)
                        {
                            RemoveCoinRecursively(doubleSpentCoin);
                        }
                    }
                    else
                    {
                        return;
                    }
                }
                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 (var doubleSpentCoin in doubleSpends)
                    {
                        RemoveCoinRecursively(doubleSpentCoin);
                    }
                }
            }

            for (var i = 0; i < tx.Transaction.Outputs.Count; i++)
            {
                // If transaction received to any of the wallet keys:
                var      output   = tx.Transaction.Outputs[i];
                HdPubKey foundKey = keys.SingleOrDefault(x => x.GetP2wpkhScript() == output.ScriptPubKey);
                if (foundKey != default)
                {
                    foundKey.KeyState = KeyState.Used;
                    var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.Transaction.RBF, foundKey.Label, spenderTransactionId: null, locked: false);                     // Don't inherit locked status from key, that's different.
                    Coins.Add(coin);

                    // Make sure there's always 21 clean keys generated and indexed.
                    if (AssertCleanKeysIndexed(21, foundKey.IsInternal()))
                    {
                        // If it generated a new key refresh the keys:
                        keys = KeyManager.GetKeys().ToList();
                    }
                }
            }

            // 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.SingleOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N);
                if (foundCoin != null)
                {
                    foundCoin.SpenderTransactionId = tx.GetHash();
                }
            }
        }
Esempio n. 26
0
    private void AnalyzeCoinjoinWalletOutputs(SmartTransaction tx, int startingOutputAnonset, ISet <HdPubKey> distinctWalletInputPubKeys)
    {
        var indistinguishableWalletOutputs = tx
                                             .WalletOutputs.GroupBy(x => x.Amount)
                                             .ToDictionary(x => x.Key, y => y.Count());

        var indistinguishableOutputs = tx.Transaction.Outputs
                                       .GroupBy(x => x.ScriptPubKey)
                                       .ToDictionary(x => x.Key, y => y.Sum(z => z.Value))
                                       .GroupBy(x => x.Value)
                                       .ToDictionary(x => x.Key, y => y.Count());

        var inputCount    = tx.Transaction.Inputs.Count;
        var ownInputCount = tx.WalletInputs.Count;

        foreach (var newCoin in tx.WalletOutputs)
        {
            var output              = newCoin.TxOut;
            var equalOutputCount    = indistinguishableOutputs[output.Value];
            var ownEqualOutputCount = indistinguishableWalletOutputs[output.Value];

            // Anonset gain cannot be larger than others' input count.
            var anonset = Math.Min(equalOutputCount - ownEqualOutputCount, inputCount - ownInputCount);

            // Picking randomly an output would make our anonset: total/ours.
            anonset /= ownEqualOutputCount;

            // Account for the inherited anonymity set size from the inputs in the
            // anonymity set size estimate.
            anonset += startingOutputAnonset;

            HdPubKey hdPubKey = newCoin.HdPubKey;
            uint256  txid     = tx.GetHash();
            if (hdPubKey.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)
            {
                // If the new coin's HD pubkey haven't been used yet
                // then its anonset haven't been set yet.
                // In that case the acquired anonset does not have to be intersected with the default anonset,
                // so this coin gets the acquired anonset.
                hdPubKey.SetAnonymitySet(anonset, txid);
            }
            else if (distinctWalletInputPubKeys.Contains(hdPubKey))
            {
                // If it's a reuse of an input's pubkey, then intersection punishment is senseless.
                hdPubKey.SetAnonymitySet(startingOutputAnonset, txid);
            }
            else if (tx.WalletOutputs.Where(x => x != newCoin).Select(x => x.HdPubKey).Contains(hdPubKey))
            {
                // If it's a reuse of another output's pubkey, then intersection punishment can only go as low as the inherited anonset.
                hdPubKey.SetAnonymitySet(Math.Max(startingOutputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet })), txid);
            }
            else if (hdPubKey.OutputAnonSetReasons.Contains(txid))
            {
                // If we already processed this transaction for this script
                // then we'll go with normal processing.
                // It may be a duplicated processing or new information arrived (like other wallet loaded)
                // If there are more anonsets already
                // then it's address reuse that we have already punished so leave it alone.
                if (hdPubKey.OutputAnonSetReasons.Count == 1)
                {
                    hdPubKey.SetAnonymitySet(anonset, txid);
                }
            }
            else
            {
                // It's address reuse.
                hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }), txid);
            }
        }
    }
Esempio n. 27
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 (var txin in tx.Transaction.Inputs)
                    {
                        if (Coins.TryGetSpenderSmartCoinsByOutPoint(txin.PrevOut, out var coins))
                        {
                            doubleSpends.AddRange(coins);
                        }
                    }

                    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
                            // spending 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 _);
                        }
                    }
                }

                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, 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. 28
0
 private void MemPoolService_TransactionReceived(object sender, SmartTransaction e)
 {
     Interlocked.Increment(ref _mempoolTransactionCount);
     Logger.LogDebug <P2pTests>($"Mempool transaction received: {e.GetHash()}.");
 }
Esempio n. 29
0
        private void ProcessTransaction(SmartTransaction tx, List <HdPubKey> keys = null)
        {
            if (tx.Height != Height.MemPool && tx.Height != Height.Unknown)
            {
                MemPool.TransactionHashes.TryRemove(tx.GetHash());
            }

            //iterate tx
            //	if already have the coin
            //		if NOT mempool
            //			update height

            //if double spend
            //	if mempool
            //		if all double spent coins are mempool and RBF
            //			remove double spent coins(if other coin spends it, remove that too and so on) // will add later if they came to our keys
            //		else
            //			return
            //	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

            //iterate tx
            //	if came to our keys
            //		add coin

            // If key list is not provided refresh the key list.
            if (keys == null)
            {
                keys = KeyManager.GetKeys().ToList();
            }

            for (var i = 0; i < tx.Transaction.Outputs.Count; i++)
            {
                // If we already had it, just update the height. Maybe got from mempool to block or reorged.
                var foundCoin = Coins.FirstOrDefault(x => x.TransactionId == tx.GetHash() && x.Index == i);
                if (foundCoin != default)
                {
                    // If tx height is mempool then don't, otherwise update the height.
                    if (tx.Height != Height.MemPool)
                    {
                        foundCoin.Height = tx.Height;
                    }
                }
            }

            // If double spend:
            List <SmartCoin> doubleSpends = Coins
                                            .Where(x => tx.Transaction.Inputs.Any(y => x.SpentOutputs.Select(z => z.ToOutPoint()).Contains(y.PrevOut)))
                                            .ToList();

            if (doubleSpends.Any())
            {
                if (tx.Height == Height.MemPool)
                {
                    // if all double spent coins are mempool and RBF
                    if (doubleSpends.All(x => x.Height == Height.MemPool && x.RBF))
                    {
                        // 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 (var doubleSpentCoin in doubleSpends)
                        {
                            RemoveCoinRecursively(doubleSpentCoin);
                        }
                    }
                    else
                    {
                        return;
                    }
                }
                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 (var doubleSpentCoin in doubleSpends)
                    {
                        RemoveCoinRecursively(doubleSpentCoin);
                    }
                }
            }

            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 = keys.SingleOrDefault(x => x.GetP2wpkhScript() == output.ScriptPubKey);
                if (foundKey != default)
                {
                    foundKey.SetKeyState(KeyState.Used, KeyManager);
                    List <SmartCoin> spentOwnCoins = Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).ToList();
                    var mixin = tx.Transaction.GetMixin(i);
                    if (spentOwnCoins.Count != 0)
                    {
                        mixin += spentOwnCoins.Min(x => x.Mixin);
                    }
                    var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.Transaction.RBF, mixin, foundKey.Label, spenderTransactionId: null, locked: false);                     // Don't inherit locked status from key, that's different.
                    Coins.TryAdd(coin);
                    if (coin.Unspent && coin.Label == "ZeroLink Change" && ChaumianClient.OnePiece != null)
                    {
                        Task.Run(async() =>
                        {
                            try
                            {
                                await ChaumianClient.QueueCoinsToMixAsync(ChaumianClient.OnePiece, coin);
                            }
                            catch (Exception ex)
                            {
                                Logger.LogError <WalletService>(ex);
                            }
                        });
                    }

                    // Make sure there's always 21 clean keys generated and indexed.
                    if (AssertCleanKeysIndexed(21, foundKey.IsInternal()))
                    {
                        // If it generated a new key refresh the keys:
                        keys = KeyManager.GetKeys().ToList();
                    }
                }
            }

            // 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)
                {
                    foundCoin.SpenderTransactionId = tx.GetHash();
                    CoinSpentOrSpenderConfirmed?.Invoke(this, foundCoin);
                }
            }
        }