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) { }