Esempio n. 1
0
        public async Task DequeueCoinsFromMixAsync(TxoRef[] coins, string reason)
        {
            if (coins is null || !coins.Any())
            {
                return;
            }

            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
            {
                try
                {
                    using (await MixLock.LockAsync(cts.Token))
                    {
                        await DequeueSpentCoinsFromMixNoLockAsync();

                        await DequeueCoinsFromMixNoLockAsync(coins, reason);
                    }
                }
                catch (TaskCanceledException)
                {
                    await DequeueSpentCoinsFromMixNoLockAsync();

                    await DequeueCoinsFromMixNoLockAsync(coins, reason);
                }
            }
        }
Esempio n. 2
0
        public async Task DequeueCoinsFromMixAsync(params TxoRef[] coins)
        {
            if (coins is null || !coins.Any())
            {
                return;
            }

            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
            {
                try
                {
                    using (await MixLock.LockAsync(cts.Token))
                    {
                        await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                        await DequeueCoinsFromMixNoLockAsync(coins);
                    }
                }
                catch (TaskCanceledException)
                {
                    await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                    await DequeueCoinsFromMixNoLockAsync(coins);
                }
            }
        }
Esempio n. 3
0
        public async Task <IEnumerable <SmartCoin> > QueueCoinsToMixAsync(string password, params SmartCoin[] coins)
        {
            using (await MixLock.LockAsync())
            {
                await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                var successful = new List <SmartCoin>();

                foreach (SmartCoin coin in coins)
                {
                    if (State.Contains(coin))
                    {
                        successful.Add(coin);
                        continue;
                    }

                    if (coin.SpentOrCoinJoinInProgress)
                    {
                        continue;
                    }

                    coin.Secret = KeyManager.GetSecrets(password, coin.ScriptPubKey).Single();
                    OnePiece    = OnePiece ?? password;

                    coin.CoinJoinInProgress = true;

                    State.AddCoinToWaitingList(coin);
                    successful.Add(coin);
                    CoinQueued?.Invoke(this, coin);
                    Logger.LogInfo <CcjClient>($"Coin queued: {coin.Index}:{coin.TransactionId}.");
                }

                return(successful);
            }
        }
Esempio n. 4
0
        public async Task DequeueCoinsFromMixAsync(IEnumerable <SmartCoin> coins, string reason)
        {
            if (coins is null || !coins.Any())
            {
                return;
            }

            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
            {
                try
                {
                    using (await MixLock.LockAsync(cts.Token))
                    {
                        await DequeueSpentCoinsFromMixNoLockAsync();

                        await DequeueCoinsFromMixNoLockAsync(coins.Select(x => x.GetTxoRef()).ToArray(), reason);
                    }
                }
                catch (TaskCanceledException)
                {
                    await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray(), reason);

                    await DequeueCoinsFromMixNoLockAsync(coins.Select(x => x.GetTxoRef()).ToArray(), reason);
                }
            }
        }
Esempio n. 5
0
        private async Task TryProcessStatusAsync(IEnumerable <RoundStateResponseBase> states)
        {
            states ??= Enumerable.Empty <RoundStateResponseBase>();

            if (Interlocked.Read(ref _statusProcessing) == 1)             // It's ok to wait for status processing next time.
            {
                return;
            }

            try
            {
                Synchronizer.BlockRequests();

                Interlocked.Exchange(ref _statusProcessing, 1);
                using (await MixLock.LockAsync().ConfigureAwait(false))
                {
                    // First, if there's delayed round registration update based on the state.
                    if (DelayedRoundRegistration != null)
                    {
                        ClientRound roundRegistered = State.GetSingleOrDefaultRound(DelayedRoundRegistration.AliceClient.RoundId);
                        roundRegistered.Registration = DelayedRoundRegistration;
                        DelayedRoundRegistration     = null;                     // Do not dispose.
                    }

                    await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false);

                    State.UpdateRoundsByStates(ExposedLinks, states.ToArray());

                    // If we do not have enough coin queued to register a round, then dequeue all.
                    ClientRound registrableRound = State.GetRegistrableRoundOrDefault();
                    if (registrableRound is { })
                    {
                        DequeueReason?reason = null;
                        // If the coordinator increases fees, do not register. Let the users register manually again.
                        if (CoordinatorFeepercentToCheck is { } && registrableRound.State.CoordinatorFeePercent > CoordinatorFeepercentToCheck)
Esempio n. 6
0
        public async Task <IEnumerable <SmartCoin> > QueueCoinsToMixAsync(string password, params SmartCoin[] coins)
        {
            if (coins is null || !coins.Any() || IsQuitPending)
            {
                return(Enumerable.Empty <SmartCoin>());
            }

            var successful = new List <SmartCoin>();

            using (await MixLock.LockAsync())
            {
                await DequeueSpentCoinsFromMixNoLockAsync();

                // Every time the user enqueues (intentionally writes in password) then the coordinator fee percent must be noted and dequeue later if changes.
                CcjClientRound latestRound = State.GetLatestRoundOrDefault();
                CoordinatorFeepercentToCheck = latestRound?.State?.CoordinatorFeePercent;

                var except = new List <SmartCoin>();

                foreach (SmartCoin coin in coins)
                {
                    if (State.Contains(coin))
                    {
                        successful.Add(coin);
                        except.Add(coin);
                        continue;
                    }

                    if (coin.Unavailable)
                    {
                        except.Add(coin);
                        continue;
                    }
                }

                var coinsExcept = coins.Except(except);
                var secPubs     = KeyManager.GetSecretsAndPubKeyPairs(password, coinsExcept.Select(x => x.ScriptPubKey).ToArray());

                Cook(password);

                foreach (SmartCoin coin in coinsExcept)
                {
                    coin.Secret = secPubs.Single(x => x.pubKey.P2wpkhScript == coin.ScriptPubKey).secret;

                    coin.CoinJoinInProgress = true;

                    State.AddCoinToWaitingList(coin);
                    successful.Add(coin);
                    Logger.LogInfo <CcjClient>($"Coin queued: {coin.Index}:{coin.TransactionId}.");
                }
            }

            foreach (var coin in successful)
            {
                CoinQueued?.Invoke(this, coin);
            }
            return(successful);
        }
Esempio n. 7
0
        public async Task DequeueCoinsFromMixAsync(params SmartCoin[] coins)
        {
            using (await MixLock.LockAsync())
            {
                await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                await DequeueCoinsFromMixNoLockAsync(coins.Select(x => (x.TransactionId, x.Index)).ToArray());
            }
        }
Esempio n. 8
0
        public async Task StopAsync()
        {
            Synchronizer.ResponseArrived -= Synchronizer_ResponseArrivedAsync;

            if (IsRunning)
            {
                Interlocked.Exchange(ref _running, 2);
            }
            Cancel?.Cancel();
            while (IsStopping)
            {
                Task.Delay(50).GetAwaiter().GetResult();                 // DO NOT MAKE IT ASYNC (.NET Core threading brainfart)
            }

            Cancel?.Dispose();

            using (await MixLock.LockAsync())
            {
                await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                State.DisposeAllAliceClients();

                IEnumerable <TxoRef> allCoins = State.GetAllQueuedCoins();
                foreach (var coinReference in allCoins)
                {
                    try
                    {
                        var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin is null)
                        {
                            continue;                             // The coin isn't present anymore. Good. This should never happen though.
                        }
                        await DequeueCoinsFromMixNoLockAsync(coin.GetTxoRef());
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError <CcjClient>("Couldn't dequeue all coins. Some coins will likely be banned from mixing.");
                        if (ex is AggregateException)
                        {
                            var aggrEx = ex as AggregateException;
                            foreach (var innerEx in aggrEx.InnerExceptions)
                            {
                                Logger.LogError <CcjClient>(innerEx);
                            }
                        }
                        else
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                }
            }
        }
Esempio n. 9
0
        public async Task StopAsync()
        {
            Synchronizer.ResponseArrived -= Synchronizer_ResponseArrivedAsync;

            Interlocked.CompareExchange(ref _running, 2, 1);             // If running, make it stopping.
            Cancel?.Cancel();
            while (Interlocked.CompareExchange(ref _running, 3, 0) == 2)
            {
                await Task.Delay(50);
            }

            Cancel?.Dispose();
            Cancel = null;

            using (await MixLock.LockAsync())
            {
                await DequeueSpentCoinsFromMixNoLockAsync();

                State.DisposeAllAliceClients();

                IEnumerable <TxoRef> allCoins = State.GetAllQueuedCoins();
                foreach (var coinReference in allCoins)
                {
                    try
                    {
                        var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin is null)
                        {
                            continue;                             // The coin isn't present anymore. Good. This should never happen though.
                        }
                        await DequeueCoinsFromMixNoLockAsync(coin.GetTxoRef(), "Stopping Wasabi.");
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError <CcjClient>("Couldn't dequeue all coins. Some coins will likely be banned from mixing.");
                        if (ex is AggregateException)
                        {
                            var aggrEx = ex as AggregateException;
                            foreach (var innerEx in aggrEx.InnerExceptions)
                            {
                                Logger.LogError <CcjClient>(innerEx);
                            }
                        }
                        else
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                }
            }
        }
Esempio n. 10
0
        private async Task ProcessStatusAsync()
        {
            try
            {
                IEnumerable <CcjRunningRoundState> states;
                int delay;
                using (await MixLock.LockAsync())
                {
                    await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                    states = await SatoshiClient.GetAllRoundStatesAsync();

                    State.UpdateRoundsByStates(states.ToArray());
                    StateUpdated?.Invoke(this, null);
                    delay = new Random().Next(0, 7);                     // delay the response to defend timing attack privacy
                }

                await Task.Delay(TimeSpan.FromSeconds(delay), Cancel.Token);

                using (await MixLock.LockAsync())
                {
                    await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                    CcjClientRound inputRegistrableRound = State.GetRegistrableRoundOrDefault();
                    if (inputRegistrableRound != null)
                    {
                        if (inputRegistrableRound.AliceClient == null)                         // If didn't register already, check what can we register.
                        {
                            await TryRegisterCoinsAsync(inputRegistrableRound);
                        }
                        else                         // We registered, let's confirm we're online.
                        {
                            await TryConfirmConnectionAsync(inputRegistrableRound);
                        }
                    }

                    foreach (long ongoingRoundId in State.GetActivelyMixingRounds())
                    {
                        await TryProcessRoundStateAsync(ongoingRoundId);
                    }
                }
            }
            catch (TaskCanceledException ex)
            {
                Logger.LogTrace <CcjClient>(ex);
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }
Esempio n. 11
0
 public async Task DequeueAllCoinsFromMixAsync(string reason)
 {
     using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
     try
     {
         using (await MixLock.LockAsync(cts.Token))
         {
             await DequeueAllCoinsFromMixNoLockAsync(reason);
         }
     }
     catch (TaskCanceledException)
     {
         await DequeueAllCoinsFromMixNoLockAsync(reason);
     }
 }
Esempio n. 12
0
 public async Task DequeueAllCoinsFromMixAsync()
 {
     using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
     {
         try
         {
             using (await MixLock.LockAsync(cts.Token))
             {
                 await DequeueCoinsFromMixNoLockAsync(State.GetAllQueuedCoins().ToArray());
             }
         }
         catch (TaskCanceledException)
         {
             await DequeueCoinsFromMixNoLockAsync(State.GetAllQueuedCoins().ToArray());
         }
     }
 }
Esempio n. 13
0
        public async Task DequeueCoinsFromMixAsync(params SmartCoin[] coins)
        {
            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
            {
                try
                {
                    using (await MixLock.LockAsync(cts.Token))
                    {
                        await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                        await DequeueCoinsFromMixNoLockAsync(coins.Select(x => (x.TransactionId, x.Index)).ToArray());
                    }
                }
                catch (TaskCanceledException)
                {
                    await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                    await DequeueCoinsFromMixNoLockAsync(coins.Select(x => (x.TransactionId, x.Index)).ToArray());
                }
            }
        }
Esempio n. 14
0
        public void Start()
        {
            if (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
            {
                return;
            }

            // The client is asking for status periodically, randomly between every 0.2 * connConfTimeout and 0.7 * connConfTimeout.
            // - if the GUI is at the mixer tab -Activate(), DeactivateIfNotMixing().
            // - if coins are queued to mix.
            // The client is asking for status periodically, randomly between every 2 to 7 seconds.
            // - if it is participating in a mix thats status >= connConf.

            // The client is triggered only when a status response arrives. The answer to the server is delayed randomly from 0 to 7 seconds.

            Task.Run(async() =>
            {
                try
                {
                    Logger.LogInfo($"{nameof(CoinJoinClient)} is successfully initialized.");

                    while (IsRunning)
                    {
                        try
                        {
                            using (await MixLock.LockAsync().ConfigureAwait(false))
                            {
                                await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false);

                                // If stop was requested return.
                                if (!IsRunning)
                                {
                                    return;
                                }

                                // if mixing >= connConf
                                if (State.GetActivelyMixingRounds().Any())
                                {
                                    int delaySeconds = new Random().Next(2, 7);
                                    Synchronizer.MaxRequestIntervalForMixing = TimeSpan.FromSeconds(delaySeconds);
                                }
                                else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || State.GetPassivelyMixingRounds().Any() || State.GetWaitingListCount() > 0)
                                {
                                    double rand      = double.Parse($"0.{new Random().Next(2, 6)}");                                // randomly between every 0.2 * connConfTimeout - 7 and 0.6 * connConfTimeout
                                    int delaySeconds = Math.Max(0, (int)((rand * State.GetSmallestRegistrationTimeout()) - 7));

                                    Synchronizer.MaxRequestIntervalForMixing = TimeSpan.FromSeconds(delaySeconds);
                                }
                                else                                 // dormant
                                {
                                    Synchronizer.MaxRequestIntervalForMixing = TimeSpan.FromMinutes(3);
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError(ex);
                        }
                        finally
                        {
                            try
                            {
                                await Task.Delay(1000, Cancel.Token).ConfigureAwait(false);
                            }
                            catch (TaskCanceledException ex)
                            {
                                Logger.LogTrace(ex);
                            }
                        }
                    }
                }
                finally
                {
                    Interlocked.CompareExchange(ref _running, 3, 2);                     // If IsStopping, make it stopped.
                }
            });
        }
Esempio n. 15
0
        public void Start(int minDelayReplySeconds, int maxDelayReplySeconds)
        {
            Interlocked.Exchange(ref _running, 1);

            // At start the client asks for status.

            // The client is asking for status periodically, randomly between every 0.2 * connConfTimeout and 0.8 * connConfTimeout.
            // - if the GUI is at the mixer tab -Activate(), DeactivateIfNotMixing().
            // - if coins are queued to mix.
            // The client is asking for status periodically, randomly between every 2 to 7 seconds.
            // - if it is participating in a mix thats status >= connConf.

            // The client is triggered only when a status response arrives. The answer to the server is delayed randomly from 0 to 7 seconds.

            Task.Run(async() =>
            {
                try
                {
                    await ProcessStatusAsync(minDelayReplySeconds, maxDelayReplySeconds);

                    Logger.LogInfo <CcjClient>("CcjClient is successfully initialized.");

                    while (IsRunning)
                    {
                        try
                        {
                            int delaySeconds;
                            using (await MixLock.LockAsync())
                            {
                                await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                                // If stop was requested return.
                                if (IsRunning == false)
                                {
                                    return;
                                }

                                // if mixing >= connConf: delay = new Random().Next(1, 3);
                                if (State.GetActivelyMixingRounds().Any())
                                {
                                    delaySeconds = new Random().Next(1, 3);
                                }
                                else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || State.GetPassivelyMixingRounds().Any() || State.GetWaitingListCount() > 0)
                                {
                                    double rand  = double.Parse($"0.{new Random().Next(1, 6)}");                                    // randomly between every 0.1 * connConfTimeout - 7 and 0.6 * connConfTimeout
                                    delaySeconds = Math.Max(0, (int)(rand * State.GetSmallestRegistrationTimeout() - 7));
                                }
                                else
                                {
                                    // dormant
                                    await Task.Delay(1000);                                     // dormant
                                    continue;
                                }
                            }

                            await Task.Delay(TimeSpan.FromSeconds(delaySeconds), Cancel.Token);
                            await ProcessStatusAsync(minDelayReplySeconds, maxDelayReplySeconds);
                        }
                        catch (TaskCanceledException ex)
                        {
                            Logger.LogTrace <CcjClient>(ex);
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                }
                finally
                {
                    if (IsStopping)
                    {
                        Interlocked.Exchange(ref _running, 3);
                    }
                }
            });
        }
Esempio n. 16
0
        private async Task ProcessStatusAsync(int minDelayReplySeconds, int maxDelayReplySeconds)
        {
            try
            {
                IEnumerable <CcjRunningRoundState> states;
                int delay;
                using (await MixLock.LockAsync())
                {
                    await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                    states = await SatoshiClient.GetAllRoundStatesAsync();

                    State.UpdateRoundsByStates(states.ToArray());

                    // If we don't have enough coin queued to register a round, then dequeue all.
                    CcjClientRound registrableRound = State.GetRegistrableRoundOrDefault();
                    if (registrableRound != default)
                    {
                        if (!registrableRound.State.HaveEnoughQueued(State.GetAllQueuedCoinAmounts().ToArray()))
                        {
                            await DequeueAllCoinsFromMixNoLockAsync();
                        }
                    }

                    StateUpdated?.Invoke(this, null);
                    if (maxDelayReplySeconds == minDelayReplySeconds)
                    {
                        delay = minDelayReplySeconds;
                    }
                    if (maxDelayReplySeconds < minDelayReplySeconds || maxDelayReplySeconds <= 0)
                    {
                        delay = 0;
                    }
                    else
                    {
                        delay = new Random().Next(minDelayReplySeconds, maxDelayReplySeconds);                         // delay the response to defend timing attack privacy
                    }
                }

                await Task.Delay(TimeSpan.FromSeconds(delay), Cancel.Token);

                using (await MixLock.LockAsync())
                {
                    foreach (long ongoingRoundId in State.GetActivelyMixingRounds())
                    {
                        await TryProcessRoundStateAsync(ongoingRoundId);
                    }

                    await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray());

                    CcjClientRound inputRegistrableRound = State.GetRegistrableRoundOrDefault();
                    if (!(inputRegistrableRound is null))
                    {
                        if (inputRegistrableRound.AliceClient is null)                         // If didn't register already, check what can we register.
                        {
                            await TryRegisterCoinsAsync(inputRegistrableRound);
                        }
                        else                         // We registered, let's confirm we're online.
                        {
                            await TryConfirmConnectionAsync(inputRegistrableRound);
                        }
                    }
                }
            }
            catch (TaskCanceledException ex)
            {
                Logger.LogTrace <CcjClient>(ex);
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }
Esempio n. 17
0
        public void Start()
        {
            Interlocked.Exchange(ref _running, 1);

            // At start the client asks for status.

            // The client is asking for status periodically, randomly between every 0.2 * connConfTimeout and 0.8 * connConfTimeout.
            // - if the GUI is at the mixer tab -Activate(), DeactivateIfNotMixing().
            // - if coins are queued to mix.
            // The client is asking for status periodically, randomly between every 2 to 7 seconds.
            // - if it is participating in a mix thats status >= connConf.

            // The client is triggered only when a status response arrives. The answer to the server is delayed randomly from 0 to 7 seconds.

            Task.Run(async() =>
            {
                try
                {
                    await ProcessStatusAsync();

                    while (IsRunning)
                    {
                        try
                        {
                            // If stop was requested return.
                            if (IsRunning == false)
                            {
                                return;
                            }

                            var inputRegMixing = false;
                            var activelyMixing = false;
                            using (await MixLock.LockAsync())
                            {
                                // if mixing >= connConf: delay = new Random().Next(2, 7);
                                activelyMixing = Rounds.Any(x => x.AliceUniqueId != null && x.State.Phase >= CcjRoundPhase.ConnectionConfirmation);
                                inputRegMixing = Rounds.Any(x => x.AliceUniqueId != null);
                            }

                            if (activelyMixing)
                            {
                                var delay = new Random().Next(2, 7);
                                await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token);
                                await ProcessStatusAsync();
                            }
                            else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || inputRegMixing)
                            {
                                double rand = double.Parse($"0.{new Random().Next(2, 8)}");                                 // randomly between every 0.2 * connConfTimeout and 0.8 * connConfTimeout
                                int delay;
                                using (await MixLock.LockAsync())
                                {
                                    delay = (int)(rand * Rounds.First(x => x.State.Phase == CcjRoundPhase.InputRegistration).State.RegistrationTimeout);
                                }

                                await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token);
                                await ProcessStatusAsync();
                            }
                            else
                            {
                                await Task.Delay(1000);                                 // dormant
                            }
                        }
                        catch (TaskCanceledException ex)
                        {
                            Logger.LogTrace <CcjClient>(ex);
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                }
                finally
                {
                    if (IsStopping)
                    {
                        Interlocked.Exchange(ref _running, 3);
                    }
                }
            });
        }
Esempio n. 18
0
        private async Task TryProcessStatusAsync(IEnumerable <CcjRunningRoundState> states)
        {
            states = states ?? Enumerable.Empty <CcjRunningRoundState>();

            if (Interlocked.Read(ref _statusProcessing) == 1)             // It's ok to wait for status processing next time.
            {
                return;
            }

            try
            {
                Synchronizer.BlockRequests();

                Interlocked.Exchange(ref _statusProcessing, 1);
                using (await MixLock.LockAsync())
                {
                    // First, if there's delayed round registration update based on the state.
                    if (DelayedRoundRegistration != null)
                    {
                        CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(DelayedRoundRegistration.AliceClient.RoundId);
                        roundRegistered.Registration = DelayedRoundRegistration;
                        DelayedRoundRegistration     = null;                     // Don't dispose.
                    }

                    await DequeueSpentCoinsFromMixNoLockAsync();

                    State.UpdateRoundsByStates(ExposedLinks, states.ToArray());

                    // If we don't have enough coin queued to register a round, then dequeue all.
                    CcjClientRound registrableRound = State.GetRegistrableRoundOrDefault();
                    if (registrableRound != default)
                    {
                        // If the coordinator increases fees, don't register. Let the users register manually again.
                        bool dequeueBecauseCoordinatorFeeChanged = false;
                        if (CoordinatorFeepercentToCheck != default)
                        {
                            dequeueBecauseCoordinatorFeeChanged = registrableRound.State.CoordinatorFeePercent > CoordinatorFeepercentToCheck;
                        }

                        if (!registrableRound.State.HaveEnoughQueued(State.GetAllQueuedCoinAmounts().ToArray()) ||
                            dequeueBecauseCoordinatorFeeChanged)
                        {
                            await DequeueAllCoinsFromMixNoLockAsync("The total value of the registered coins is not enough or the coordinator's fee changed.");
                        }
                    }
                }
                StateUpdated?.Invoke(this, null);

                int delaySeconds = new Random().Next(0, 7);                 // delay the response to defend timing attack privacy.

                if (Network == Network.RegTest)
                {
                    delaySeconds = 0;
                }

                await Task.Delay(TimeSpan.FromSeconds(delaySeconds), Cancel.Token);

                using (await MixLock.LockAsync())
                {
                    foreach (long ongoingRoundId in State.GetActivelyMixingRounds())
                    {
                        await TryProcessRoundStateAsync(ongoingRoundId);
                    }

                    await DequeueSpentCoinsFromMixNoLockAsync();

                    CcjClientRound inputRegistrableRound = State.GetRegistrableRoundOrDefault();
                    if (inputRegistrableRound != null)
                    {
                        if (inputRegistrableRound.Registration is null)                         // If did not register already, check what can we register.
                        {
                            await TryRegisterCoinsAsync(inputRegistrableRound);
                        }
                        else                         // We registered, let's confirm we're online.
                        {
                            await TryConfirmConnectionAsync(inputRegistrableRound);
                        }
                    }
                }
            }
            catch (TaskCanceledException ex)
            {
                Logger.LogTrace <CcjClient>(ex);
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
            finally
            {
                Interlocked.Exchange(ref _statusProcessing, 0);
                Synchronizer.EnableRequests();
            }
        }
Esempio n. 19
0
        private async Task ProcessStatusAsync()
        {
            try
            {
                IEnumerable <CcjRunningRoundState> states = await SatoshiClient.GetAllRoundStatesAsync();

                using (await MixLock.LockAsync())
                {
                    foreach (CcjRunningRoundState state in states)
                    {
                        CcjClientRound round = Rounds.SingleOrDefault(x => x.State.RoundId == state.RoundId);
                        if (round == null)                         // It's a new running round.
                        {
                            var r = new CcjClientRound(state);
                            Rounds.Add(r);
                            RoundAdded?.Invoke(this, r);
                        }
                        else
                        {
                            round.State = state;
                            RoundUpdated?.Invoke(this, round);
                        }
                    }

                    var roundsToRemove = new List <long>();
                    foreach (CcjClientRound round in Rounds)
                    {
                        CcjRunningRoundState state = states.SingleOrDefault(x => x.RoundId == round.State.RoundId);
                        if (state == null)                         // The round is not running anymore.
                        {
                            foreach (MixCoin rc in round.CoinsRegistered)
                            {
                                CoinsWaitingForMix.Add(rc);
                            }
                            roundsToRemove.Add(round.State.RoundId);
                        }
                    }

                    foreach (long roundId in roundsToRemove)
                    {
                        Rounds.RemoveAll(x => x.State.RoundId == roundId);
                        RoundRemoved?.Invoke(this, roundId);
                    }
                }

                int delay = new Random().Next(0, 7);                 // delay the response to defend timing attack privacy
                await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token);

                using (await MixLock.LockAsync())
                {
                    CoinsWaitingForMix.RemoveAll(x => x.SmartCoin.SpenderTransactionId != null);                     // Make sure coins those were somehow spent are removed.

                    CcjClientRound inputRegistrableRound = Rounds.First(x => x.State.Phase == CcjRoundPhase.InputRegistration);
                    if (inputRegistrableRound.AliceUniqueId == null)                     // If didn't register already, check what can we register.
                    {
                        try
                        {
                            var   coinsToRegister             = new List <MixCoin>();
                            var   amountSoFar                 = Money.Zero;
                            Money amountNeededExceptInputFees = inputRegistrableRound.State.Denomination + inputRegistrableRound.State.FeePerOutputs * 2;
                            var   tooSmallInputs              = false;
                            foreach (MixCoin coin in CoinsWaitingForMix
                                     .Where(x => x.SmartCoin.Confirmed || x.SmartCoin.Label.Contains("CoinJoin", StringComparison.Ordinal)) // Where our label contains CoinJoin, CoinJoins can be registered even if not confirmed, our label will likely be CoinJoin only if it was a previous CoinJoin, otherwise the server will refuse us.
                                     .OrderByDescending(y => y.SmartCoin.Amount)                                                            // First order by amount.
                                     .OrderByDescending(z => z.SmartCoin.Confirmed))                                                        // Then order by the amount ordered ienumerable by confirmation, so first try to register confirmed coins.
                            {
                                coinsToRegister.Add(coin);
                                if (inputRegistrableRound.State.MaximumInputCountPerPeer < coinsToRegister.Count)
                                {
                                    tooSmallInputs = true;
                                    break;
                                }

                                amountSoFar += coin.SmartCoin.Amount;
                                if (amountSoFar > amountNeededExceptInputFees + inputRegistrableRound.State.FeePerInputs * coinsToRegister.Count)
                                {
                                    break;
                                }
                            }

                            // If input count doesn't reach the max input registration AND there are enough coins queued, then register to mix.
                            if (!tooSmallInputs && amountSoFar > amountNeededExceptInputFees + inputRegistrableRound.State.FeePerInputs * coinsToRegister.Count)
                            {
                                var changeKey = KeyManager.GenerateNewKey("CoinJoin Change Output", KeyState.Locked, isInternal: true);
                                var activeKey = KeyManager.GenerateNewKey("CoinJoin Active Output", KeyState.Locked, isInternal: true);
                                var blind     = BlindingPubKey.Blind(activeKey.GetP2wpkhScript().ToBytes());

                                var inputProofs = new List <InputProofModel>();
                                foreach (var coin in coinsToRegister)
                                {
                                    var inputProof = new InputProofModel
                                    {
                                        Input = coin.SmartCoin.GetOutPoint(),
                                        Proof = coin.Secret.PrivateKey.SignMessage(ByteHelpers.ToHex(blind.BlindedData))
                                    };
                                    inputProofs.Add(inputProof);
                                }
                                InputsResponse inputsResponse = await AliceClient.PostInputsAsync(changeKey.GetP2wpkhScript(), blind.BlindedData, inputProofs.ToArray());

                                if (!BlindingPubKey.Verify(inputsResponse.BlindedOutputSignature, blind.BlindedData))
                                {
                                    throw new NotSupportedException("Coordinator did not sign the blinded output properly.");
                                }

                                CcjClientRound roundRegistered = Rounds.SingleOrDefault(x => x.State.RoundId == inputsResponse.RoundId);
                                if (roundRegistered == null)
                                {
                                    // If our SatoshiClient doesn't yet know about the round because of the dealy create it.
                                    // Make its state as it'd be the same as our assumed round was, except the roundId and registeredPeerCount, it'll be updated later.
                                    roundRegistered = new CcjClientRound(CcjRunningRoundState.CloneExcept(inputRegistrableRound.State, inputsResponse.RoundId, registeredPeerCount: 1));
                                    Rounds.Add(roundRegistered);
                                    RoundAdded?.Invoke(this, roundRegistered);
                                }

                                foreach (var coin in coinsToRegister)
                                {
                                    roundRegistered.CoinsRegistered.Add(coin);
                                    CoinsWaitingForMix.Remove(coin);
                                }
                                roundRegistered.ActiveOutput       = activeKey;
                                roundRegistered.ChangeOutput       = changeKey;
                                roundRegistered.UnblindedSignature = BlindingPubKey.UnblindSignature(inputsResponse.BlindedOutputSignature, blind.BlindingFactor);
                                roundRegistered.AliceUniqueId      = inputsResponse.UniqueId;
                                RoundUpdated?.Invoke(this, roundRegistered);
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                    else                     // We registered, let's confirm we're online.
                    {
                        try
                        {
                            string roundHash = await AliceClient.PostConfirmationAsync(inputRegistrableRound.State.RoundId, (Guid)inputRegistrableRound.AliceUniqueId);

                            if (roundHash != null)                             // Then the phase went to connection confirmation.
                            {
                                inputRegistrableRound.RoundHash   = roundHash;
                                inputRegistrableRound.State.Phase = CcjRoundPhase.ConnectionConfirmation;
                                RoundUpdated?.Invoke(this, inputRegistrableRound);
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }

                    foreach (CcjClientRound ongoingRound in Rounds.Where(x => x.State.Phase != CcjRoundPhase.InputRegistration && x.AliceUniqueId != null))
                    {
                        try
                        {
                            if (ongoingRound.State.Phase == CcjRoundPhase.ConnectionConfirmation)
                            {
                                if (ongoingRound.RoundHash == null)                                 // If we didn't already obtained our roundHash obtain it.
                                {
                                    string roundHash = await AliceClient.PostConfirmationAsync(inputRegistrableRound.State.RoundId, (Guid)inputRegistrableRound.AliceUniqueId);

                                    if (roundHash == null)
                                    {
                                        throw new NotSupportedException("Coordinator didn't gave us the expected roundHash, even though it's in ConnectionConfirmation phase.");
                                    }
                                    else
                                    {
                                        ongoingRound.RoundHash = roundHash;
                                        RoundUpdated?.Invoke(this, ongoingRound);
                                    }
                                }
                            }
                            else if (ongoingRound.State.Phase == CcjRoundPhase.OutputRegistration)
                            {
                                if (ongoingRound.RoundHash == null)
                                {
                                    throw new NotSupportedException("Coordinator progressed to OutputRegistration phase, even though we didn't obtain roundHash.");
                                }

                                await BobClient.PostOutputAsync(ongoingRound.RoundHash, ongoingRound.ActiveOutput.GetP2wpkhScript(), ongoingRound.UnblindedSignature);
                            }
                            else if (ongoingRound.State.Phase == CcjRoundPhase.Signing)
                            {
                                Transaction unsignedCoinJoin = await AliceClient.GetUnsignedCoinJoinAsync(ongoingRound.State.RoundId, (Guid)ongoingRound.AliceUniqueId);

                                if (NBitcoinHelpers.HashOutpoints(unsignedCoinJoin.Inputs.Select(x => x.PrevOut)) != ongoingRound.RoundHash)
                                {
                                    throw new NotSupportedException("Coordinator provided invalid roundHash.");
                                }
                                Money amountBack = unsignedCoinJoin.Outputs
                                                   .Where(x => x.ScriptPubKey == ongoingRound.ActiveOutput.GetP2wpkhScript() || x.ScriptPubKey == ongoingRound.ChangeOutput.GetP2wpkhScript())
                                                   .Sum(y => y.Value);
                                Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.SmartCoin.Amount);                                              // Start with input sum.
                                minAmountBack -= ongoingRound.State.FeePerOutputs * 2 + ongoingRound.State.FeePerInputs * ongoingRound.CoinsRegistered.Count; // Minus miner fee.
                                Money actualDenomination     = unsignedCoinJoin.GetIndistinguishableOutputs().OrderByDescending(x => x.count).First().value;  // Denomination may grow.
                                Money expectedCoordinatorFee = new Money((ongoingRound.State.CoordinatorFeePercent * 0.01m) * decimal.Parse(actualDenomination.ToString(false, true)), MoneyUnit.BTC);
                                minAmountBack -= expectedCoordinatorFee;                                                                                      // Minus expected coordinator fee.

                                // If there's no change output then coordinator protection may happened:
                                if (unsignedCoinJoin.Outputs.All(x => x.ScriptPubKey != ongoingRound.ChangeOutput.GetP2wpkhScript()))
                                {
                                    Money minimumOutputAmount      = new Money(0.0001m, MoneyUnit.BTC);                                             // If the change would be less than about $1 then add it to the coordinator.
                                    Money onePercentOfDenomination = new Money(actualDenomination.ToDecimal(MoneyUnit.BTC) * 0.01m, MoneyUnit.BTC); // If the change is less than about 1% of the newDenomination then add it to the coordinator fee.
                                    Money minimumChangeAmount      = Math.Max(minimumOutputAmount, onePercentOfDenomination);

                                    minAmountBack -= minimumChangeAmount;                                     // Minus coordinator protections (so it won't create bad coinjoins.)
                                }

                                if (amountBack < minAmountBack)
                                {
                                    throw new NotSupportedException("Coordinator did not add enough value to our outputs in the coinjoin.");
                                }

                                new TransactionBuilder()
                                .AddKeys(ongoingRound.CoinsRegistered.Select(x => x.Secret).ToArray())
                                .AddCoins(ongoingRound.CoinsRegistered.Select(x => x.SmartCoin.GetCoin()))
                                .SignTransactionInPlace(unsignedCoinJoin, SigHash.All);

                                var myDic = new Dictionary <int, WitScript>();

                                for (int i = 0; i < unsignedCoinJoin.Inputs.Count; i++)
                                {
                                    var input = unsignedCoinJoin.Inputs[i];
                                    if (ongoingRound.CoinsRegistered.Select(x => x.SmartCoin.GetOutPoint()).Contains(input.PrevOut))
                                    {
                                        myDic.Add(i, unsignedCoinJoin.Inputs[i].WitScript);
                                    }
                                }

                                await AliceClient.PostSignaturesAsync(ongoingRound.State.RoundId, (Guid)ongoingRound.AliceUniqueId, myDic);
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                }
            }
            catch (TaskCanceledException ex)
            {
                Logger.LogTrace <CcjClient>(ex);
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }