示例#1
0
        public async Task ScanAsync(bool rescan)
        {
            if (rescan)
            {
                Logger.LogWarning("Rescanning...");
            }
            if (rescan && Directory.Exists(WorkFolder))
            {
                Directory.Delete(WorkFolder, true);
            }
            Directory.CreateDirectory(WorkFolder);
            var allWasabiCoinJoinSet   = new HashSet <uint256>();
            var allSamouraiCoinJoinSet = new HashSet <uint256>();
            var allOtherCoinJoinSet    = new HashSet <uint256>();
            var allSamouraiTx0Set      = new HashSet <uint256>();

            var opreturnTransactionCache = new MemoryCache(new MemoryCacheOptions()
            {
                SizeLimit = 100000
            });

            ulong startingHeight = Constants.FirstWasabiBlock;
            ulong height         = startingHeight;

            if (File.Exists(LastProcessedBlockHeightPath))
            {
                height = ReadBestHeight() + 1;
                allSamouraiCoinJoinSet = Enumerable.ToHashSet(ReadSamouraiCoinJoins().Select(x => x.Id));
                allWasabiCoinJoinSet   = Enumerable.ToHashSet(ReadWasabiCoinJoins().Select(x => x.Id));
                allOtherCoinJoinSet    = Enumerable.ToHashSet(ReadOtherCoinJoins().Select(x => x.Id));
                allSamouraiTx0Set      = Enumerable.ToHashSet(ReadSamouraiTx0s().Select(x => x.Id));
                Logger.LogWarning($"{height - startingHeight + 1} blocks already processed. Continue scanning...");
            }

            var bestHeight = (ulong)await Rpc.GetBlockCountAsync().ConfigureAwait(false);

            Logger.LogInfo($"Last processed block: {height - 1}.");
            ulong totalBlocks = bestHeight - height + 1;

            Logger.LogInfo($"About {totalBlocks} ({totalBlocks / 144} days) blocks will be processed.");

            var stopWatch = new Stopwatch();
            var processedBlocksWhenSwStarted = CalculateProcessedBlocks(height, bestHeight, totalBlocks);

            stopWatch.Start();

            while (height <= bestHeight)
            {
                var block = await Rpc.GetVerboseBlockAsync(height, safe : false).ConfigureAwait(false);

                var wasabiCoinJoins         = new List <VerboseTransactionInfo>();
                var samouraiCoinJoins       = new List <VerboseTransactionInfo>();
                var samouraiTx0s            = new List <VerboseTransactionInfo>();
                var otherCoinJoins          = new List <VerboseTransactionInfo>();
                var wasabiPostMixTxs        = new List <VerboseTransactionInfo>();
                var samouraiPostMixTxs      = new List <VerboseTransactionInfo>();
                var otherCoinJoinPostMixTxs = new List <VerboseTransactionInfo>();

                foreach (var tx in block.Transactions)
                {
                    if (tx.Outputs.Count() > 2 && tx.Outputs.Any(x => TxNullDataTemplate.Instance.CheckScriptPubKey(x.ScriptPubKey)))
                    {
                        opreturnTransactionCache.Set(tx.Id, tx, new MemoryCacheEntryOptions().SetSize(1));
                    }

                    bool isWasabiCj   = false;
                    bool isSamouraiCj = false;
                    bool isOtherCj    = false;
                    if (tx.Inputs.All(x => x.Coinbase is null))
                    {
                        var indistinguishableOutputs = tx.GetIndistinguishableOutputs(includeSingle: false).ToArray();
                        if (indistinguishableOutputs.Any())
                        {
                            var outputs      = tx.Outputs.ToArray();
                            var inputs       = tx.Inputs.Select(x => x.PrevOutput).ToArray();
                            var outputValues = outputs.Select(x => x.Value);
                            var inputValues  = inputs.Select(x => x.Value);
                            var outputCount  = outputs.Length;
                            var inputCount   = inputs.Length;
                            (Money mostFrequentEqualOutputValue, int mostFrequentEqualOutputCount) = indistinguishableOutputs.OrderByDescending(x => x.count).First();
                            // IDENTIFY WASABI COINJOINS
                            if (block.Height >= Constants.FirstWasabiBlock)
                            {
                                // Before Wasabi had constant coordinator addresses and different base denominations at the beginning.
                                if (block.Height < Constants.FirstWasabiNoCoordAddressBlock)
                                {
                                    isWasabiCj = tx.Outputs.Any(x => Constants.WasabiCoordScripts.Contains(x.ScriptPubKey)) && indistinguishableOutputs.Any(x => x.count > 2);
                                }
                                else
                                {
                                    isWasabiCj =
                                        mostFrequentEqualOutputCount >= 10 && // At least 10 equal outputs.
                                        inputCount >= mostFrequentEqualOutputCount && // More inptu than outputs.
                                        mostFrequentEqualOutputValue.Almost(Constants.ApproximateWasabiBaseDenomination, Constants.WasabiBaseDenominationPrecision);    // The most frequent equal outputs must be almost the base denomination.
                                }
                            }

                            // IDENTIFY SAMOURAI COINJOINS
                            if (block.Height >= Constants.FirstSamouraiBlock)
                            {
                                isSamouraiCj =
                                    inputCount == 5 && // Always have 5 inputs.
                                    outputCount == 5 && // Always have 5 outputs.
                                    outputValues.Distinct().Count() == 1 && // Outputs are always equal.
                                    Constants.SamouraiPools.Any(x => x.Almost(tx.Outputs.First().Value, Money.Coins(0.01m)));   // Just to be sure match Samourai's pool sizes.
                            }

                            // IDENTIFY OTHER EQUAL OUTPUT COINJOIN LIKE TRANSACTIONS
                            if (!isWasabiCj && !isSamouraiCj)
                            {
                                isOtherCj =
                                    indistinguishableOutputs.Length == 1 && // If it isn't then it'd be likely a multidenomination CJ, which only Wasabi does.
                                    mostFrequentEqualOutputCount == outputCount - mostFrequentEqualOutputCount && // Rarely it isn't, but it helps filtering out false positives.
                                    outputs.Select(x => x.ScriptPubKey).Distinct().Count() >= mostFrequentEqualOutputCount && // Otherwise more participants would be single actors which makes no sense.
                                    inputs.Select(x => x.ScriptPubKey).Distinct().Count() >= mostFrequentEqualOutputCount && // Otherwise more participants would be single actors which makes no sense.
                                    inputValues.Max() <= mostFrequentEqualOutputValue + outputValues.Where(x => x != mostFrequentEqualOutputValue).Max() - Money.Coins(0.0001m);    // I don't want to run expensive subset sum, so this is a shortcut to at least filter out false positives.
                            }

                            if (isWasabiCj)
                            {
                                wasabiCoinJoins.Add(tx);
                                allWasabiCoinJoinSet.Add(tx.Id);
                            }
                            else if (isSamouraiCj)
                            {
                                samouraiCoinJoins.Add(tx);
                                allSamouraiCoinJoinSet.Add(tx.Id);
                            }
                            else if (isOtherCj)
                            {
                                otherCoinJoins.Add(tx);
                                allOtherCoinJoinSet.Add(tx.Id);
                            }
                        }

                        foreach (var inputTxId in tx.Inputs.Select(x => x.OutPoint.Hash))
                        {
                            if (!isWasabiCj && allWasabiCoinJoinSet.Contains(inputTxId) && !wasabiPostMixTxs.Any(x => x.Id == tx.Id))
                            {
                                // Then it's a post mix tx.
                                wasabiPostMixTxs.Add(tx);
                                if (isOtherCj)
                                {
                                    // Then it's false positive detection.
                                    isOtherCj = false;
                                    allOtherCoinJoinSet.Remove(tx.Id);
                                    otherCoinJoins.Remove(tx);
                                }
                            }

                            if (!isSamouraiCj && allSamouraiCoinJoinSet.Contains(inputTxId) && !samouraiPostMixTxs.Any(x => x.Id == tx.Id))
                            {
                                // Then it's a post mix tx.
                                samouraiPostMixTxs.Add(tx);
                                if (isOtherCj)
                                {
                                    // Then it's false positive detection.
                                    isOtherCj = false;
                                    allOtherCoinJoinSet.Remove(tx.Id);
                                    otherCoinJoins.Remove(tx);
                                }
                            }

                            if (!isOtherCj && allOtherCoinJoinSet.Contains(inputTxId) && !otherCoinJoinPostMixTxs.Any(x => x.Id == tx.Id))
                            {
                                // Then it's a post mix tx.
                                otherCoinJoinPostMixTxs.Add(tx);
                            }
                        }
                    }
                }

                foreach (var txid in samouraiCoinJoins.SelectMany(x => x.Inputs).Select(x => x.OutPoint.Hash).Where(x => !allSamouraiCoinJoinSet.Contains(x) && !allSamouraiTx0Set.Contains(x)).Distinct())
                {
                    if (!opreturnTransactionCache.TryGetValue(txid, out VerboseTransactionInfo vtxi))
                    {
                        var tx0 = await Rpc.GetSmartRawTransactionInfoAsync(txid).ConfigureAwait(false);

                        var verboseOutputs = new List <VerboseOutputInfo>(tx0.Transaction.Outputs.Count);
                        foreach (var o in tx0.Transaction.Outputs)
                        {
                            var voi = new VerboseOutputInfo(o.Value, o.ScriptPubKey);
                            verboseOutputs.Add(voi);
                        }

                        var verboseInputs = new List <VerboseInputInfo>(tx0.Transaction.Inputs.Count);
                        foreach (var i in tx0.Transaction.Inputs)
                        {
                            var tx = await Rpc.GetRawTransactionAsync(i.PrevOut.Hash).ConfigureAwait(false);

                            var o   = tx.Outputs[i.PrevOut.N];
                            var voi = new VerboseOutputInfo(o.Value, o.ScriptPubKey);
                            var vii = new VerboseInputInfo(i.PrevOut, voi);
                            verboseInputs.Add(vii);
                        }

                        vtxi = new VerboseTransactionInfo(tx0.TransactionBlockInfo, txid, verboseInputs, verboseOutputs);
                    }

                    if (vtxi is { })
 public VerboseInputInfo(OutPoint outPoint, VerboseOutputInfo prevOutput)
     : this(outPoint, prevOutput, null)
 {
 }