Пример #1
0
 public async Task <IEnumerable <uint256> > GetUnconfirmedCoinJoinsAsync()
 {
     using (await CoinJoinsLock.LockAsync())
     {
         return(UnconfirmedCoinJoins.ToArray());
     }
 }
Пример #2
0
        public async Task ProcessConfirmedTransactionAsync(Transaction tx)
        {
            // This should not be needed until we would only accept unconfirmed CJ outputs an no other unconf outs. But it'll be more bulletproof for future extensions.
            // Turns out you shouldn't accept RBF at all never. (See below.)

            // https://github.com/zkSNACKs/WalletWasabi/issues/145
            // if it spends a banned output AND it's not CJ output
            // ban all the outputs of the transaction
            tx.PrecomputeHash(false, true);

            UnconfirmedCoinJoins.Remove(tx.GetHash());             // Locked outside.

            if (RoundConfig.DosSeverity <= 1)
            {
                return;
            }

            foreach (TxIn input in tx.Inputs)
            {
                OutPoint prevOut = input.PrevOut;

                // if coin is not banned
                var foundElem = await UtxoReferee.TryGetBannedAsync(prevOut, notedToo : true).ConfigureAwait(false);

                if (foundElem is { })
Пример #3
0
 public async Task <bool> ContainsUnconfirmedCoinJoinAsync(uint256 hash)
 {
     using (await CoinJoinsLock.LockAsync().ConfigureAwait(false))
     {
         return(UnconfirmedCoinJoins.Contains(hash));
     }
 }
Пример #4
0
        public async Task <bool> IsUnconfirmedCoinJoinLimitReachedAsync()
        {
            using (await CoinJoinsLock.LockAsync())
            {
                if (UnconfirmedCoinJoins.Count() < 24)
                {
                    return(false);
                }
                else
                {
                    foreach (var cjHash in UnconfirmedCoinJoins)
                    {
                        RPCResponse getRawTransactionResponse = await RpcClient.SendCommandAsync(RPCOperations.getrawtransaction, cjHash.ToString(), true);

                        // if failed remove from everywhere (should not happen normally)
                        if (string.IsNullOrWhiteSpace(getRawTransactionResponse?.ResultString))
                        {
                            UnconfirmedCoinJoins.Remove(cjHash);
                            CoinJoins.Remove(cjHash);
                            await File.WriteAllLinesAsync(CoinJoinsFilePath, CoinJoins.Select(x => x.ToString()));
                        }
                        // if confirmed remove only from unconfirmed
                        if (getRawTransactionResponse.Result.Value <int>("confirmations") > 0)
                        {
                            UnconfirmedCoinJoins.Remove(cjHash);
                        }
                    }
                }

                return(UnconfirmedCoinJoins.Count() >= 24);
            }
        }
Пример #5
0
        public async Task <bool> IsUnconfirmedCoinJoinLimitReachedAsync()
        {
            using (await CoinJoinsLock.LockAsync())
            {
                if (UnconfirmedCoinJoins.Count < 24)
                {
                    return(false);
                }
                foreach (var cjHash in UnconfirmedCoinJoins.ToArray())
                {
                    try
                    {
                        var txInfo = await RpcClient.GetRawTransactionInfoAsync(cjHash);

                        // if confirmed remove only from unconfirmed
                        if (txInfo.Confirmations > 0)
                        {
                            UnconfirmedCoinJoins.Remove(cjHash);
                        }
                    }
                    catch (Exception ex)
                    {
                        // If aborted remove from everywhere (should not happen normally).
                        UnconfirmedCoinJoins.Remove(cjHash);
                        CoinJoins.Remove(cjHash);
                        await File.WriteAllLinesAsync(CoinJoinsFilePath, CoinJoins.Select(x => x.ToString()));

                        Logger.LogWarning <CcjCoordinator>(ex);
                    }
                }

                return(UnconfirmedCoinJoins.Count >= 24);
            }
        }
        public async Task ProcessConfirmedTransactionAsync(Transaction tx)
        {
            // This should not be needed until we would only accept unconfirmed CJ outputs an no other unconf outs. But it'll be more bulletproof for future extensions.
            // Turns out you shouldn't accept RBF at all never. (See below.)

            // https://github.com/zkSNACKs/WalletWasabi/issues/145
            // if it spends a banned output AND it's not CJ output
            // ban all the outputs of the transaction
            tx.PrecomputeHash(false, true);

            UnconfirmedCoinJoins.Remove(tx.GetHash());             // Locked outside.

            if (RoundConfig.DosSeverity <= 1)
            {
                return;
            }

            foreach (TxIn input in tx.Inputs)
            {
                OutPoint prevOut = input.PrevOut;

                // if coin is not banned
                var foundElem = await UtxoReferee.TryGetBannedAsync(prevOut, notedToo : true).ConfigureAwait(false);

                if (foundElem != null)
                {
                    if (!AnyRunningRoundContainsInput(prevOut, out _))
                    {
                        int newSeverity = foundElem.Severity + 1;
                        await UtxoReferee.UnbanAsync(prevOut).ConfigureAwait(false);                         // since it's not an UTXO anymore

                        if (RoundConfig.DosSeverity >= newSeverity)
                        {
                            var txCoins = tx.Outputs.AsIndexedOutputs().Select(x => x.ToCoin().Outpoint);
                            await UtxoReferee.BanUtxosAsync(newSeverity, foundElem.TimeOfBan, forceNoted : foundElem.IsNoted, foundElem.BannedForRound, txCoins.ToArray()).ConfigureAwait(false);
                        }
                    }
                }
            }
        }
        private async void Round_StatusChangedAsync(object sender, CoordinatorRoundStatus status)
        {
            try
            {
                var round = sender as CoordinatorRound;

                Money feePerInputs  = null;
                Money feePerOutputs = null;

                // If success save the coinjoin.
                if (status == CoordinatorRoundStatus.Succeded)
                {
                    uint256[] mempoolHashes = null;
                    try
                    {
                        mempoolHashes = await RpcClient.GetRawMempoolAsync().ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError(ex);
                    }

                    using (await CoinJoinsLock.LockAsync().ConfigureAwait(false))
                    {
                        if (mempoolHashes is { })
                        {
                            var fallOuts = UnconfirmedCoinJoins.Where(x => !mempoolHashes.Contains(x));
                            CoinJoins.RemoveAll(x => fallOuts.Contains(x));
                            UnconfirmedCoinJoins.RemoveAll(x => fallOuts.Contains(x));
                        }

                        uint256 coinJoinHash = round.CoinJoin.GetHash();
                        CoinJoins.Add(coinJoinHash);
                        UnconfirmedCoinJoins.Add(coinJoinHash);
                        LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow;
                        await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() }).ConfigureAwait(false);

                        // When a round succeeded, adjust the denomination as to users still be able to register with the latest round's active output amount.
                        IEnumerable <(Money value, int count)> outputs = round.CoinJoin.GetIndistinguishableOutputs(includeSingle: true);
                        var bestOutput = outputs.OrderByDescending(x => x.count).FirstOrDefault();
                        if (bestOutput != default)
                        {
                            Money activeOutputAmount = bestOutput.value;

                            int currentConfirmationTarget = await AdjustConfirmationTargetAsync(lockCoinJoins : false).ConfigureAwait(false);

                            var fees = await CoordinatorRound.CalculateFeesAsync(RpcClient, currentConfirmationTarget).ConfigureAwait(false);

                            feePerInputs  = fees.feePerInputs;
                            feePerOutputs = fees.feePerOutputs;

                            Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + (2 * feePerOutputs));
                            if (newDenominationToGetInWithactiveOutputs < RoundConfig.Denomination)
                            {
                                if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m))
                                {
                                    RoundConfig.Denomination = newDenominationToGetInWithactiveOutputs;
                                    RoundConfig.ToFile();
                                }
                            }
                        }
                    }
Пример #8
0
        private async void Round_StatusChangedAsync(object sender, CoordinatorRoundStatus status)
        {
            try
            {
                var round = sender as CoordinatorRound;

                Money feePerInputs  = null;
                Money feePerOutputs = null;

                // If success save the coinjoin.
                if (status == CoordinatorRoundStatus.Succeded)
                {
                    using (await CoinJoinsLock.LockAsync().ConfigureAwait(false))
                    {
                        uint256 coinJoinHash = round.CoinJoin.GetHash();
                        CoinJoins.Add(coinJoinHash);
                        UnconfirmedCoinJoins.Add(coinJoinHash);
                        LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow;
                        await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() }).ConfigureAwait(false);

                        // When a round succeeded, adjust the denomination as to users still be able to register with the latest round's active output amount.
                        IEnumerable <(Money value, int count)> outputs = round.CoinJoin.GetIndistinguishableOutputs(includeSingle: true);
                        var bestOutput = outputs.OrderByDescending(x => x.count).FirstOrDefault();
                        if (bestOutput != default)
                        {
                            Money activeOutputAmount = bestOutput.value;

                            int currentConfirmationTarget = await AdjustConfirmationTargetAsync(lockCoinJoins : false).ConfigureAwait(false);

                            var fees = await CoordinatorRound.CalculateFeesAsync(RpcClient, currentConfirmationTarget).ConfigureAwait(false);

                            feePerInputs  = fees.feePerInputs;
                            feePerOutputs = fees.feePerOutputs;

                            Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + (2 * feePerOutputs));
                            if (newDenominationToGetInWithactiveOutputs < RoundConfig.Denomination)
                            {
                                if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m))
                                {
                                    RoundConfig.Denomination = newDenominationToGetInWithactiveOutputs;
                                    RoundConfig.ToFile();
                                }
                            }
                        }
                    }
                }

                // If aborted in signing phase, then ban Alices that did not sign.
                if (status == CoordinatorRoundStatus.Aborted && round.Phase == RoundPhase.Signing)
                {
                    IEnumerable <Alice> alicesDidntSign = round.GetAlicesByNot(AliceState.SignedCoinJoin, syncLock: false);

                    CoordinatorRound nextRound = GetCurrentInputRegisterableRoundOrDefault(syncLock: false);

                    if (nextRound != null)
                    {
                        int nextRoundAlicesCount = nextRound.CountAlices(syncLock: false);
                        var alicesSignedCount    = round.AnonymitySet - alicesDidntSign.Count();

                        // New round's anonset should be the number of alices that signed in this round.
                        // Except if the number of alices in the next round is already larger.
                        var newAnonymitySet = Math.Max(alicesSignedCount, nextRoundAlicesCount);
                        // But it cannot be larger than the current anonset of that round.
                        newAnonymitySet = Math.Min(newAnonymitySet, nextRound.AnonymitySet);

                        // Only change the anonymity set of the next round if new anonset does not equal and newanonset is larger than 1.
                        if (nextRound.AnonymitySet != newAnonymitySet && newAnonymitySet > 1)
                        {
                            nextRound.UpdateAnonymitySet(newAnonymitySet, syncLock: false);

                            if (nextRoundAlicesCount >= nextRound.AnonymitySet)
                            {
                                // Progress to the next phase, which will be OutputRegistration
                                await nextRound.ExecuteNextPhaseAsync(RoundPhase.ConnectionConfirmation).ConfigureAwait(false);
                            }
                        }
                    }

                    foreach (Alice alice in alicesDidntSign)                     // Because the event sometimes is raised from inside the lock.
                    {
                        // If it is from any coinjoin, then do not ban.
                        IEnumerable <OutPoint> utxosToBan = alice.Inputs.Select(x => x.Outpoint);
                        await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted : false, round.RoundId, utxosToBan.ToArray()).ConfigureAwait(false);
                    }
                }

                // If finished start a new round.
                if (status == CoordinatorRoundStatus.Aborted || status == CoordinatorRoundStatus.Succeded)
                {
                    round.StatusChanged       -= Round_StatusChangedAsync;
                    round.CoinJoinBroadcasted -= Round_CoinJoinBroadcasted;
                    await MakeSureTwoRunningRoundsAsync(feePerInputs, feePerOutputs).ConfigureAwait(false);
                }
            }
            catch (Exception ex)
            {
                Logger.LogWarning(ex);
            }
        }
Пример #9
0
        private volatile bool _disposedValue = false;         // To detect redundant calls

        public Coordinator(Network network, BlockNotifier blockNotifier, string folderPath, IRPCClient rpc, CoordinatorRoundConfig roundConfig)
        {
            Network       = Guard.NotNull(nameof(network), network);
            BlockNotifier = Guard.NotNull(nameof(blockNotifier), blockNotifier);
            FolderPath    = Guard.NotNullOrEmptyOrWhitespace(nameof(folderPath), folderPath, trim: true);
            RpcClient     = Guard.NotNull(nameof(rpc), rpc);
            RoundConfig   = Guard.NotNull(nameof(roundConfig), roundConfig);

            Rounds         = new List <CoordinatorRound>();
            RoundsListLock = new AsyncLock();

            LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow;

            Directory.CreateDirectory(FolderPath);

            UtxoReferee = new UtxoReferee(Network, FolderPath, RpcClient, RoundConfig);

            if (File.Exists(CoinJoinsFilePath))
            {
                try
                {
                    var getTxTasks = new List <(Task <Transaction> txTask, string line)>();
                    var batch      = RpcClient.PrepareBatch();

                    var      toRemove = new List <string>();
                    string[] allLines = File.ReadAllLines(CoinJoinsFilePath);
                    foreach (string line in allLines)
                    {
                        try
                        {
                            getTxTasks.Add((batch.GetRawTransactionAsync(uint256.Parse(line)), line));
                        }
                        catch (Exception ex)
                        {
                            toRemove.Add(line);

                            var logEntry = ex is RPCException rpce && rpce.RPCCode == RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY
                                                                ? $"CoinJoins file contains invalid transaction ID {line}"
                                                                : $"CoinJoins file got corrupted. Deleting offending line \"{line.Substring(0, 20)}\".";

                            Logger.LogWarning($"{logEntry}. {ex.GetType()}: {ex.Message}");
                        }
                    }

                    batch.SendBatchAsync().GetAwaiter().GetResult();

                    foreach (var(txTask, line) in getTxTasks)
                    {
                        try
                        {
                            var tx = txTask.GetAwaiter().GetResult();
                            CoinJoins.Add(tx.GetHash());
                        }
                        catch (Exception ex)
                        {
                            toRemove.Add(line);

                            var logEntry = ex is RPCException rpce && rpce.RPCCode == RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY
                                                                ? $"CoinJoins file contains invalid transaction ID {line}"
                                                                : $"CoinJoins file got corrupted. Deleting offending line \"{line.Substring(0, 20)}\".";

                            Logger.LogWarning($"{logEntry}. {ex.GetType()}: {ex.Message}");
                        }
                    }

                    if (toRemove.Count != 0)                     // a little performance boost, it'll be empty almost always
                    {
                        var newAllLines = allLines.Where(x => !toRemove.Contains(x));
                        File.WriteAllLines(CoinJoinsFilePath, newAllLines);
                    }
                }
                catch (Exception ex)
                {
                    Logger.LogWarning($"CoinJoins file got corrupted. Deleting {CoinJoinsFilePath}. {ex.GetType()}: {ex.Message}");
                    File.Delete(CoinJoinsFilePath);
                }

                uint256[] mempoolHashes = RpcClient.GetRawMempoolAsync().GetAwaiter().GetResult();
                UnconfirmedCoinJoins.AddRange(CoinJoins.Intersect(mempoolHashes));
            }

            try
            {
                string roundCountFilePath = Path.Combine(folderPath, "RoundCount.txt");
                if (File.Exists(roundCountFilePath))
                {
                    string roundCount = File.ReadAllText(roundCountFilePath);
                    CoordinatorRound.RoundCount = long.Parse(roundCount);
                }
                else
                {
                    // First time initializes (so the first constructor will increment it and we'll start from 1.)
                    CoordinatorRound.RoundCount = 0;
                }
            }
            catch (Exception ex)
            {
                CoordinatorRound.RoundCount = 0;
                Logger.LogInfo($"{nameof(CoordinatorRound.RoundCount)} file was corrupt. Resetting to 0.");
                Logger.LogDebug(ex);
            }

            BlockNotifier.OnBlock += BlockNotifier_OnBlockAsync;
        }