private async Task TryConfirmConnectionAsync(CcjClientRound inputRegistrableRound)
        {
            try
            {
                var res = await inputRegistrableRound.Registration.AliceClient.PostConfirmationAsync();

                if (res.activeOutputs.Any())
                {
                    inputRegistrableRound.Registration.ActiveOutputs = res.activeOutputs;
                }

                if (res.currentPhase > CcjRoundPhase.InputRegistration)                 // Then the phase went to connection confirmation (probably).
                {
                    inputRegistrableRound.Registration.SetPhaseCompleted(CcjRoundPhase.ConnectionConfirmation);
                    inputRegistrableRound.State.Phase = res.currentPhase;
                }
            }
            catch (Exception ex)
            {
                if (ex.Message.StartsWith("Not Found", StringComparison.Ordinal))                 // Alice timed out.
                {
                    State.ClearRoundRegistration(inputRegistrableRound.State.RoundId);
                }
                Logger.LogError <CcjClient>(ex);
            }
        }
Exemple #2
0
		private void UpdateRequiredBtcLabel(CcjClientRound registrableRound)
		{
			if (Global.WalletService is null)
			{
				return; // Otherwise NullReferenceException at shutdown.
			}

			if (registrableRound == default)
			{
				if (RequiredBTC == default)
				{
					RequiredBTC = Money.Zero;
				}
			}
			else
			{
				var queued = Global.WalletService.Coins.Where(x => x.CoinJoinInProgress);
				if (queued.Any())
				{
					RequiredBTC = registrableRound.State.CalculateRequiredAmount(Global.ChaumianClient.State.GetAllQueuedCoinAmounts().ToArray());
				}
				else
				{
					var available = Global.WalletService.Coins.Where(x => x.Confirmed && !x.Unavailable);
					RequiredBTC = available.Any()
						? registrableRound.State.CalculateRequiredAmount(available.Where(x => x.AnonymitySet < Global.Config.PrivacyLevelStrong).Select(x => x.Amount).ToArray())
						: registrableRound.State.CalculateRequiredAmount();
				}
			}
		}
Exemple #3
0
        private Dictionary <int, WitScript> SignCoinJoin(CcjClientRound ongoingRound, Transaction unsignedCoinJoin)
        {
            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.ActiveOutputAddress.ScriptPubKey || x.ScriptPubKey == ongoingRound.ChangeOutputAddress.ScriptPubKey)
                               .Select(y => y.Value)
                               .Sum();
            Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.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 = actualDenomination.Percentange(ongoingRound.State.CoordinatorFeePercent) * ongoingRound.State.RequiredPeerCount;

            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.ChangeOutputAddress.ScriptPubKey))
            {
                Money minimumOutputAmount      = Money.Coins(0.0001m);               // If the change would be less than about $1 then add it to the coordinator.
                Money onePercentOfDenomination = actualDenomination.Percentange(1m); // 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.");
            }

            var signedCoinJoin = unsignedCoinJoin.Clone();

            signedCoinJoin.Sign(ongoingRound.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(OnePiece, x.ScriptPubKey).Single()).ToArray(), ongoingRound.CoinsRegistered.Select(x => x.GetCoin()).ToArray());

            // Old way of signing, which randomly fails! https://github.com/zkSNACKs/WalletWasabi/issues/716#issuecomment-435498906
            // Must be fixed in NBitcoin.
            //var builder = Network.CreateTransactionBuilder();
            //var signedCoinJoin = builder
            //	.ContinueToBuild(unsignedCoinJoin)
            //	.AddKeys(ongoingRound.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(OnePiece, x.ScriptPubKey).Single()).ToArray())
            //	.AddCoins(ongoingRound.CoinsRegistered.Select(x => x.GetCoin()))
            //	.BuildTransaction(true);

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

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

            return(myDic);
        }
        public override void OnOpen()
        {
            base.OnOpen();

            if (Disposables != null)
            {
                throw new Exception("CoinJoin tab opened before previous closed.");
            }

            Disposables = new CompositeDisposable();

            TargetPrivacy = Global.Config.GetTargetPrivacy();

            var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault();

            UpdateRequiredBtcLabel(registrableRound);

            CoordinatorFeePercent = registrableRound?.State?.CoordinatorFeePercent.ToString() ?? "0.003";

            Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.CoinQueued))
            .Merge(Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.CoinDequeued)))
            .Merge(Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.StateUpdated)))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => UpdateStates())
            .DisposeWith(Disposables);

            CcjClientRound mostAdvancedRound = Global.ChaumianClient?.State?.GetMostAdvancedRoundOrDefault();

            if (mostAdvancedRound != default)
            {
                RoundId         = mostAdvancedRound.State.RoundId;
                Phase           = mostAdvancedRound.State.Phase;
                RoundTimesout   = mostAdvancedRound.State.Phase == CcjRoundPhase.InputRegistration ? mostAdvancedRound.State.InputRegistrationTimesout : DateTimeOffset.UtcNow;
                PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount;
                PeersNeeded     = mostAdvancedRound.State.RequiredPeerCount;
            }
            else
            {
                RoundId         = -1;
                Phase           = CcjRoundPhase.InputRegistration;
                RoundTimesout   = DateTimeOffset.UtcNow;
                PeersRegistered = 0;
                PeersNeeded     = 100;
            }

            Global.UiConfig.WhenAnyValue(x => x.LurkingWifeMode).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
            {
                this.RaisePropertyChanged(nameof(AmountQueued));
                this.RaisePropertyChanged(nameof(IsLurkingWifeMode));
            }).DisposeWith(Disposables);

            Observable.Interval(TimeSpan.FromSeconds(1))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ =>
            {
                TimeSpan left            = RoundTimesout - DateTimeOffset.UtcNow;
                TimeLeftTillRoundTimeout = left > TimeSpan.Zero ? left : TimeSpan.Zero;                         // Make sure cannot be less than zero.
            }).DisposeWith(Disposables);
        }
        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);
        }
Exemple #6
0
        private async Task RegisterOutputAsync(CcjClientRound ongoingRound)
        {
            using (var bobClient = new BobClient(CcjHostUri, TorSocks5EndPoint))
            {
                await bobClient.PostOutputAsync(ongoingRound.RoundHash, ongoingRound.ActiveOutputAddress, ongoingRound.UnblindedSignature);

                ongoingRound.PostedOutput = true;
                Logger.LogInfo <AliceClient>($"Round ({ongoingRound.State.RoundId}) Bob Posted output.");
            }
        }
Exemple #7
0
        public override void OnOpen()
        {
            CoinsList.OnOpen();

            if (Disposables != null)
            {
                throw new Exception("CoinJoin tab opened before previous closed.");
            }

            Disposables = new CompositeDisposable();

            TargetPrivacy = GetTargetPrivacy(Global.Config.MixUntilAnonymitySet);

            var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault();

            UpdateRequiredBtcLabel(registrableRound);

            CoordinatorFeePercent = registrableRound?.State?.CoordinatorFeePercent.ToString() ?? "0.003";

            Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.CoinQueued))
            .Merge(Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.CoinDequeued)))
            .Merge(Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.StateUpdated)))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => UpdateStates())
            .DisposeWith(Disposables);

            CcjClientRound mostAdvancedRound = Global.ChaumianClient?.State?.GetMostAdvancedRoundOrDefault();

            if (mostAdvancedRound != default)
            {
                RoundId = mostAdvancedRound.State.RoundId;
                SuccessfulRoundCount = mostAdvancedRound.State.SuccessfulRoundCount;
                Phase           = mostAdvancedRound.State.Phase;
                PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount;
                PeersNeeded     = mostAdvancedRound.State.RequiredPeerCount;
            }
            else
            {
                RoundId = -1;
                SuccessfulRoundCount = -1;
                Phase           = CcjRoundPhase.InputRegistration;
                PeersRegistered = 0;
                PeersNeeded     = 100;
            }

            Global.UiConfig.WhenAnyValue(x => x.LurkingWifeMode).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
            {
                this.RaisePropertyChanged(nameof(AmountQueued));
                this.RaisePropertyChanged(nameof(IsLurkingWifeMode));
            }).DisposeWith(Disposables);

            base.OnOpen();
        }
Exemple #8
0
        private static Dictionary <int, WitScript> SignCoinJoin(CcjClientRound ongoingRound, Transaction unsignedCoinJoin)
        {
            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.ActiveOutputAddress.ScriptPubKey || x.ScriptPubKey == ongoingRound.ChangeOutputAddress.ScriptPubKey)
                               .Select(y => y.Value)
                               .Sum();
            Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.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 = actualDenomination.Percentange(ongoingRound.State.CoordinatorFeePercent);

            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.ChangeOutputAddress.ScriptPubKey))
            {
                Money minimumOutputAmount      = Money.Coins(0.0001m);               // If the change would be less than about $1 then add it to the coordinator.
                Money onePercentOfDenomination = actualDenomination.Percentange(1m); // 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.");
            }

            var builder        = new TransactionBuilder();
            var signedCoinJoin = builder
                                 .ContinueToBuild(unsignedCoinJoin)
                                 .AddKeys(ongoingRound.CoinsRegistered.Select(x => x.Secret).ToArray())
                                 .AddCoins(ongoingRound.CoinsRegistered.Select(x => x.GetCoin()))
                                 .BuildTransaction(true);

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

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

            return(myDic);
        }
Exemple #9
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);
            }
        }
Exemple #10
0
        private async Task TryConfirmConnectionAsync(CcjClientRound inputRegistrableRound)
        {
            try
            {
                string roundHash = await inputRegistrableRound.AliceClient.PostConfirmationAsync();

                if (!(roundHash is null))                 // Then the phase went to connection confirmation.
                {
                    inputRegistrableRound.RoundHash   = roundHash;
                    inputRegistrableRound.State.Phase = CcjRoundPhase.ConnectionConfirmation;
                }
            }
            catch (Exception ex)
            {
                if (ex.Message.StartsWith("Not Found", StringComparison.Ordinal))                 // Alice timed out.
                {
                    State.ClearRoundRegistration(inputRegistrableRound.State.RoundId);
                }
                Logger.LogError <CcjClient>(ex);
            }
        }
        private async Task RegisterOutputAsync(CcjClientRound ongoingRound)
        {
            IEnumerable <TxoRef> registeredInputs = ongoingRound.Registration.CoinsRegistered.Select(x => x.GetTxoRef());

            var shuffledOutputs = ongoingRound.Registration.ActiveOutputs.ToList();

            shuffledOutputs.Shuffle();
            foreach (var activeOutput in shuffledOutputs)
            {
                using (var bobClient = new BobClient(CcjHostUriAction, TorSocks5EndPoint))
                {
                    if (!await bobClient.PostOutputAsync(ongoingRound.RoundId, activeOutput))
                    {
                        Logger.LogWarning <AliceClient>($"Round ({ongoingRound.State.RoundId}) Bobs did not have enough time to post outputs before timeout. If you see this message, contact nopara73, so he can optimize the phase timeout periods to the worst Internet/Tor connections, which may be yours.)");
                        break;
                    }

                    // Unblind our exposed links.
                    foreach (TxoRef input in registeredInputs)
                    {
                        if (ExposedLinks.ContainsKey(input))                         // Should never not contain, but oh well, let's not disrupt the round for this.
                        {
                            var found = ExposedLinks[input].FirstOrDefault(x => x.Key.GetP2wpkhAddress(Network) == activeOutput.Address);
                            if (found != default)
                            {
                                found.IsBlinded = false;
                            }
                            else
                            {
                                // Should never happen, but oh well we can autocorrect it so why not.
                                ExposedLinks[input] = ExposedLinks[input].Append(new HdPubKeyBlindedPair(KeyManager.GetKeyForScriptPubKey(activeOutput.Address.ScriptPubKey), false));
                            }
                        }
                    }
                }
            }

            ongoingRound.Registration.SetPhaseCompleted(CcjRoundPhase.OutputRegistration);
            Logger.LogInfo <AliceClient>($"Round ({ongoingRound.State.RoundId}) Bob Posted outputs: {ongoingRound.Registration.ActiveOutputs.Count()}.");
        }
        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();
            }
        }
Exemple #13
0
        private static async Task ObtainRoundHashAsync(CcjClientRound ongoingRound)
        {
            string roundHash = await ongoingRound.AliceClient.PostConfirmationAsync();

            ongoingRound.RoundHash = roundHash ?? throw new NotSupportedException($"Coordinator didn't gave us the expected {nameof(roundHash)}, even though it's in ConnectionConfirmation phase.");
        }
Exemple #14
0
        public CoinJoinTabViewModel(WalletViewModel walletViewModel)
            : base("CoinJoin", walletViewModel)
        {
            Password      = "";
            TargetPrivacy = GetTargetPrivacy(Global.Config.MixUntilAnonymitySet);

            var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault();

            UpdateRequiredBtcLabel(registrableRound);

            CoordinatorFeePercent = registrableRound?.State?.CoordinatorFeePercent.ToString() ?? "0.003";

            CoinsList = new CoinListViewModel().DisposeWith(Disposables);

            AmountQueued = Money.Zero;             // Global.ChaumianClient.State.SumAllQueuedCoinAmounts();

            Global.ChaumianClient.CoinQueued   += ChaumianClient_CoinQueued;
            Global.ChaumianClient.CoinDequeued += ChaumianClient_CoinDequeued;

            CcjClientRound mostAdvancedRound = Global.ChaumianClient?.State?.GetMostAdvancedRoundOrDefault();

            if (mostAdvancedRound != default)
            {
                RoundId = mostAdvancedRound.State.RoundId;
                SuccessfulRoundCount = mostAdvancedRound.State.SuccessfulRoundCount;
                Phase           = mostAdvancedRound.State.Phase;
                PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount;
                PeersNeeded     = mostAdvancedRound.State.RequiredPeerCount;
            }
            else
            {
                RoundId = -1;
                SuccessfulRoundCount = -1;
                Phase           = CcjRoundPhase.InputRegistration;
                PeersRegistered = 0;
                PeersNeeded     = 100;
            }

            Global.ChaumianClient.StateUpdated += ChaumianClient_StateUpdated;

            EnqueueCommand = ReactiveCommand.Create(async() =>
            {
                await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected));
            }).DisposeWith(Disposables);

            DequeueCommand = ReactiveCommand.Create(async() =>
            {
                await DoDequeueAsync(CoinsList.Coins.Where(c => c.IsSelected));
            }).DisposeWith(Disposables);

            PrivacySomeCommand = ReactiveCommand.Create(() =>
            {
                TargetPrivacy = TargetPrivacy.Some;
            }).DisposeWith(Disposables);

            PrivacyFineCommand = ReactiveCommand.Create(() =>
            {
                TargetPrivacy = TargetPrivacy.Fine;
            }).DisposeWith(Disposables);

            PrivacyStrongCommand = ReactiveCommand.Create(() =>
            {
                TargetPrivacy = TargetPrivacy.Strong;
            }).DisposeWith(Disposables);

            TargetButtonCommand = ReactiveCommand.Create(async() =>
            {
                switch (TargetPrivacy)
                {
                case TargetPrivacy.None:
                    TargetPrivacy = TargetPrivacy.Some;
                    break;

                case TargetPrivacy.Some:
                    TargetPrivacy = TargetPrivacy.Fine;
                    break;

                case TargetPrivacy.Fine:
                    TargetPrivacy = TargetPrivacy.Strong;
                    break;

                case TargetPrivacy.Strong:
                    TargetPrivacy = TargetPrivacy.Some;
                    break;
                }
                Global.Config.MixUntilAnonymitySet = CoinJoinUntilAnonimitySet;
                await Global.Config.ToFileAsync();
            }).DisposeWith(Disposables);

            this.WhenAnyValue(x => x.Password).Subscribe(async x =>
            {
                try
                {
                    if (x.NotNullAndNotEmpty())
                    {
                        char lastChar = x.Last();
                        if (lastChar == '\r' || lastChar == '\n')                         // If the last character is cr or lf then act like it'd be a sign to do the job.
                        {
                            Password = x.TrimEnd('\r', '\n');
                            await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Logger.LogTrace(ex);
                }
            }).DisposeWith(Disposables);

            this.WhenAnyValue(x => x.IsEnqueueBusy).Subscribe(busy =>
            {
                if (busy)
                {
                    EnqueueButtonText = EnqueuingButtonTextString;
                }
                else
                {
                    EnqueueButtonText = EnqueueButtonTextString;
                }
            }).DisposeWith(Disposables);

            this.WhenAnyValue(x => x.IsDequeueBusy).Subscribe(busy =>
            {
                if (busy)
                {
                    DequeueButtonText = DequeuingButtonTextString;
                }
                else
                {
                    DequeueButtonText = DequeueButtonTextString;
                }
            }).DisposeWith(Disposables);

            this.WhenAnyValue(x => x.TargetPrivacy).Subscribe(target =>
            {
                CoinJoinUntilAnonimitySet = GetTargetLevel(target);
            }).DisposeWith(Disposables);
        }
Exemple #15
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);
            }
        }
Exemple #16
0
        private SmartCoinStatus GetSmartCoinStatus()
        {
            if (Model.IsBanned)
            {
                return(SmartCoinStatus.MixingBanned);
            }

            CcjClientState clientState = Global.ChaumianClient.State;

            if (Model.CoinJoinInProgress)
            {
                foreach (long roundId in clientState.GetAllMixingRounds())
                {
                    CcjClientRound round = clientState.GetSingleOrDefaultRound(roundId);
                    if (round != default)
                    {
                        if (round.CoinsRegistered.Contains(Model))
                        {
                            if (round.State.Phase == CcjRoundPhase.InputRegistration)
                            {
                                return(SmartCoinStatus.MixingInputRegistration);
                            }
                            else if (round.State.Phase == CcjRoundPhase.ConnectionConfirmation)
                            {
                                return(SmartCoinStatus.MixingConnectionConfirmation);
                            }
                            else if (round.State.Phase == CcjRoundPhase.OutputRegistration)
                            {
                                return(SmartCoinStatus.MixingOutputRegistration);
                            }
                            else if (round.State.Phase == CcjRoundPhase.Signing)
                            {
                                return(SmartCoinStatus.MixingSigning);
                            }
                        }
                    }
                }
            }

            if (Model.SpentAccordingToBackend)
            {
                return(SmartCoinStatus.SpentAccordingToBackend);
            }

            if (Model.Confirmed)
            {
                if (Model.CoinJoinInProgress)
                {
                    return(SmartCoinStatus.MixingOnWaitingList);
                }
                else
                {
                    return(SmartCoinStatus.Confirmed);
                }
            }
            else             // Unconfirmed
            {
                if (Model.CoinJoinInProgress)
                {
                    return(SmartCoinStatus.MixingWaitingForConfirmation);
                }
                else
                {
                    return(SmartCoinStatus.Unconfirmed);
                }
            }
        }
        private async Task TryRegisterCoinsAsync(CcjClientRound inputRegistrableRound)
        {
            try
            {
                // Select the most suitable coins to regiter.
                List <TxoRef> registrableCoins = State.GetRegistrableCoins(
                    inputRegistrableRound.State.MaximumInputCountPerPeer,
                    inputRegistrableRound.State.Denomination,
                    inputRegistrableRound.State.FeePerInputs,
                    inputRegistrableRound.State.FeePerOutputs).ToList();

                // If there are no suitable coins to register return.
                if (!registrableCoins.Any())
                {
                    return;
                }

                (HdPubKey change, IEnumerable <HdPubKey> actives)outputAddresses = GetOutputsToRegister(inputRegistrableRound.State.Denomination, inputRegistrableRound.State.SchnorrPubKeys.Count(), registrableCoins);

                SchnorrPubKey[]  schnorrPubKeys = inputRegistrableRound.State.SchnorrPubKeys.ToArray();
                List <Requester> requesters     = new List <Requester>();
                var blindedOutputScriptHashes   = new List <uint256>();

                var registeredAddresses = new List <BitcoinAddress>();
                for (int i = 0; i < schnorrPubKeys.Length; i++)
                {
                    if (outputAddresses.actives.Count() <= i)
                    {
                        break;
                    }

                    BitcoinAddress address = outputAddresses.actives.Select(x => x.GetP2wpkhAddress(Network)).ElementAt(i);

                    SchnorrPubKey schnorrPubKey           = schnorrPubKeys[i];
                    var           outputScriptHash        = new uint256(Hashes.SHA256(address.ScriptPubKey.ToBytes()));
                    var           requester               = new Requester();
                    uint256       blindedOutputScriptHash = requester.BlindMessage(outputScriptHash, schnorrPubKey);
                    requesters.Add(requester);
                    blindedOutputScriptHashes.Add(blindedOutputScriptHash);
                    registeredAddresses.Add(address);
                }

                byte[]  blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputScriptHashes.Select(x => x.ToBytes()));
                uint256 blindedOutputScriptsHash      = new uint256(Hashes.SHA256(blindedOutputScriptHashesByte));

                var inputProofs = new List <InputProofModel>();
                foreach (TxoRef coinReference in registrableCoins)
                {
                    SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                    if (coin is null)
                    {
                        throw new NotSupportedException("This is impossible.");
                    }

                    coin.Secret = coin.Secret ?? KeyManager.GetSecrets(SaltSoup(), coin.ScriptPubKey).Single();
                    var inputProof = new InputProofModel {
                        Input = coin.GetTxoRef(),
                        Proof = coin.Secret.PrivateKey.SignCompact(blindedOutputScriptsHash)
                    };
                    inputProofs.Add(inputProof);
                }

                AliceClient aliceClient = null;
                try
                {
                    aliceClient = await AliceClient.CreateNewAsync(inputRegistrableRound.RoundId, registeredAddresses, schnorrPubKeys, requesters, Network, outputAddresses.change.GetP2wpkhAddress(Network), blindedOutputScriptHashes, inputProofs, CcjHostUriAction, TorSocks5EndPoint);
                }
                catch (HttpRequestException ex) when(ex.Message.Contains("Input is banned", StringComparison.InvariantCultureIgnoreCase))
                {
                    string[] parts             = ex.Message.Split(new[] { "Input is banned from participation for ", " minutes: " }, StringSplitOptions.RemoveEmptyEntries);
                    string   minutesString     = parts[1];
                    int      minuteInt         = int.Parse(minutesString);
                    string   bannedInputString = parts[2].TrimEnd('.');

                    string[]  bannedInputStringParts = bannedInputString.Split(':', StringSplitOptions.RemoveEmptyEntries);
                    TxoRef    coinReference          = new TxoRef(new uint256(bannedInputStringParts[1]), uint.Parse(bannedInputStringParts[0]));
                    SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);

                    if (coin is null)
                    {
                        throw new NotSupportedException("This is impossible.");
                    }

                    coin.BannedUntilUtc = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(minuteInt);

                    Logger.LogWarning <CcjClient>(ex.Message.Split('\n')[1]);

                    await DequeueCoinsFromMixNoLockAsync(coinReference, "Failed to register the coin with the coordinator.");

                    aliceClient?.Dispose();
                    return;
                }
                catch (HttpRequestException ex) when(ex.Message.Contains("Provided input is not unspent", StringComparison.InvariantCultureIgnoreCase))
                {
                    string[] parts            = ex.Message.Split(new[] { "Provided input is not unspent: " }, StringSplitOptions.RemoveEmptyEntries);
                    string   spentInputString = parts[1].TrimEnd('.');

                    string[]  bannedInputStringParts = spentInputString.Split(':', StringSplitOptions.RemoveEmptyEntries);
                    TxoRef    coinReference          = new TxoRef(new uint256(bannedInputStringParts[1]), uint.Parse(bannedInputStringParts[0]));
                    SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);

                    if (coin is null)
                    {
                        throw new NotSupportedException("This is impossible.");
                    }

                    coin.SpentAccordingToBackend = true;

                    Logger.LogWarning <CcjClient>(ex.Message.Split('\n')[1]);

                    await DequeueCoinsFromMixNoLockAsync(coinReference, "Failed to register the coin with the coordinator. The coin is already spent.");

                    aliceClient?.Dispose();
                    return;
                }
                catch (HttpRequestException ex) when(ex.Message.Contains("No such running round in InputRegistration.", StringComparison.InvariantCultureIgnoreCase))
                {
                    Logger.LogInfo <CcjClient>("Client tried to register a round that is not in InputRegistration anymore. Trying again later.");
                    aliceClient?.Dispose();
                    return;
                }
                catch (HttpRequestException ex) when(ex.Message.Contains("too-long-mempool-chain", StringComparison.InvariantCultureIgnoreCase))
                {
                    Logger.LogInfo <CcjClient>("Coordinator failed because too much unconfirmed parent transactions. Trying again later.");
                    aliceClient?.Dispose();
                    return;
                }

                var coinsRegistered = new List <SmartCoin>();
                foreach (TxoRef coinReference in registrableCoins)
                {
                    var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                    if (coin is null)
                    {
                        throw new NotSupportedException("This is impossible.");
                    }

                    coinsRegistered.Add(coin);
                    State.RemoveCoinFromWaitingList(coin);
                }

                var registration = new ClientRoundRegistration(aliceClient, coinsRegistered, outputAddresses.change.GetP2wpkhAddress(Network));

                CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId);
                if (roundRegistered is null)
                {
                    // If our SatoshiClient does not yet know about the round, because of delay, then delay the round registration.
                    DelayedRoundRegistration?.Dispose();
                    DelayedRoundRegistration = registration;
                }

                roundRegistered.Registration = registration;
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }
Exemple #18
0
        private async Task TryRegisterCoinsAsync(CcjClientRound inputRegistrableRound)
        {
            try
            {
                List <(uint256 txid, uint index)> registrableCoins = State.GetRegistrableCoins(
                    inputRegistrableRound.State.MaximumInputCountPerPeer,
                    inputRegistrableRound.State.Denomination,
                    inputRegistrableRound.State.FeePerInputs,
                    inputRegistrableRound.State.FeePerOutputs).ToList();

                if (registrableCoins.Any())
                {
                    BitcoinAddress changeAddress = null;
                    BitcoinAddress activeAddress = null;
                    lock (CustomChangeAddressesLock)
                    {
                        if (CustomChangeAddresses.Count > 0)
                        {
                            changeAddress = CustomChangeAddresses.First();
                            CustomChangeAddresses.RemoveFirst();
                        }
                    }
                    lock (CustomActiveAddressesLock)
                    {
                        if (CustomActiveAddresses.Count > 0)
                        {
                            activeAddress = CustomActiveAddresses.First();
                            CustomActiveAddresses.RemoveFirst();
                        }
                    }
                    changeAddress = changeAddress ?? KeyManager.GenerateNewKey("ZeroLink Change", KeyState.Locked, isInternal: true, toFile: false).GetP2wpkhAddress(Network);
                    activeAddress = activeAddress ?? KeyManager.GenerateNewKey("ZeroLink Mixed Coin", KeyState.Locked, isInternal: true, toFile: false).GetP2wpkhAddress(Network);
                    KeyManager.ToFile();

                    var blind = CoordinatorPubKey.Blind(activeAddress.ScriptPubKey.ToBytes());

                    var inputProofs = new List <InputProofModel>();
                    foreach ((uint256 txid, uint index)coinReference in registrableCoins)
                    {
                        var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin == null)
                        {
                            throw new NotSupportedException("This is impossible.");
                        }
                        var inputProof = new InputProofModel
                        {
                            Input = coin.GetTxoRef(),
                            Proof = coin.Secret.PrivateKey.SignMessage(ByteHelpers.ToHex(blind.BlindedData))
                        };
                        inputProofs.Add(inputProof);
                    }
                    AliceClient aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blind.BlindedData, inputProofs, CcjHostUri, TorSocks5EndPoint);

                    byte[] unblindedSignature = CoordinatorPubKey.UnblindSignature(aliceClient.BlindedOutputSignature, blind.BlindingFactor);

                    if (!CoordinatorPubKey.Verify(unblindedSignature, activeAddress.ScriptPubKey.ToBytes()))
                    {
                        throw new NotSupportedException("Coordinator did not sign the blinded output properly.");
                    }

                    CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.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, aliceClient.RoundId, registeredPeerCount: 1));
                        State.AddOrReplaceRound(roundRegistered);
                    }

                    foreach ((uint256 txid, uint index)coinReference in registrableCoins)
                    {
                        var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin == null)
                        {
                            throw new NotSupportedException("This is impossible.");
                        }
                        roundRegistered.CoinsRegistered.Add(coin);
                        State.RemoveCoinFromWaitingList(coin);
                    }
                    roundRegistered.ActiveOutputAddress = activeAddress;
                    roundRegistered.ChangeOutputAddress = changeAddress;
                    roundRegistered.UnblindedSignature  = unblindedSignature;
                    roundRegistered.AliceClient         = aliceClient;
                }
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }
Exemple #19
0
        private Dictionary <int, WitScript> SignCoinJoin(CcjClientRound ongoingRound, Transaction unsignedCoinJoin)
        {
            TxOut[] myOutputs = unsignedCoinJoin.Outputs
                                .Where(x => x.ScriptPubKey == ongoingRound.Registration.ChangeAddress.ScriptPubKey ||
                                       ongoingRound.Registration.ActiveOutputs.Select(y => y.address.ScriptPubKey).Contains(x.ScriptPubKey))
                                .ToArray();
            Money amountBack = myOutputs.Sum(y => y.Value);

            // Make sure change is counted.
            Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.Amount);                                                                                      // Start with input sum.

            minAmountBack -= ongoingRound.State.FeePerOutputs * myOutputs.Length + ongoingRound.State.FeePerInputs * ongoingRound.Registration.CoinsRegistered.Count(); // Minus miner fee.

            IOrderedEnumerable <(Money value, int count)> indistinguishableOutputs = unsignedCoinJoin.GetIndistinguishableOutputs(includeSingle: false).OrderByDescending(x => x.count);

            foreach ((Money value, int count)denomPair in indistinguishableOutputs)
            {
                if (myOutputs.Select(x => x.Value)
                    .Contains(denomPair.value))
                {
                    Money denomination           = denomPair.value;
                    Money expectedCoordinatorFee = denomination.Percentange(ongoingRound.State.CoordinatorFeePercent * denomPair.count);
                    minAmountBack -= expectedCoordinatorFee;                     // Minus expected coordinator fee.
                }
            }

            // If there's no change output then coordinator protection may happened:
            if (!myOutputs.Select(x => x.ScriptPubKey).Contains(ongoingRound.Registration.ChangeAddress.ScriptPubKey))
            {
                Money minimumOutputAmount      = Money.Coins(0.0001m);             // If the change would be less than about $1 then add it to the coordinator.
                Money baseDenomination         = indistinguishableOutputs.First().value;
                Money onePercentOfDenomination = baseDenomination.Percentange(1m); // 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.");
            }

            var signedCoinJoin = unsignedCoinJoin.Clone();

            signedCoinJoin.Sign(ongoingRound.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(SaltSoup(), x.ScriptPubKey).Single()).ToArray(), ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin()).ToArray());

            // Old way of signing, which randomly fails! https://github.com/zkSNACKs/WalletWasabi/issues/716#issuecomment-435498906
            // Must be fixed in NBitcoin.
            //var builder = Network.CreateTransactionBuilder();
            //var signedCoinJoin = builder
            //	.ContinueToBuild(unsignedCoinJoin)
            //	.AddKeys(ongoingRound.Registration.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(OnePiece, x.ScriptPubKey).Single()).ToArray())
            //	.AddCoins(ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin()))
            //	.BuildTransaction(true);

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

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

            return(myDic);
        }
Exemple #20
0
        private async Task TryRegisterCoinsAsync(CcjClientRound inputRegistrableRound)
        {
            try
            {
                List <(uint256 txid, uint index)> registrableCoins = State.GetRegistrableCoins(
                    inputRegistrableRound.State.MaximumInputCountPerPeer,
                    inputRegistrableRound.State.Denomination,
                    inputRegistrableRound.State.FeePerInputs,
                    inputRegistrableRound.State.FeePerOutputs).ToList();

                if (registrableCoins.Any())
                {
                    BitcoinAddress changeAddress = null;
                    BitcoinAddress activeAddress = null;
                    lock (CustomChangeAddressesLock)
                    {
                        if (CustomChangeAddresses.Count > 0)
                        {
                            changeAddress = CustomChangeAddresses.First();
                            CustomChangeAddresses.RemoveFirst();
                        }
                    }
                    lock (CustomActiveAddressesLock)
                    {
                        if (CustomActiveAddresses.Count > 0)
                        {
                            activeAddress = CustomActiveAddresses.First();
                            CustomActiveAddresses.RemoveFirst();
                        }
                    }

                    if (changeAddress is null || activeAddress is null)
                    {
                        IEnumerable <HdPubKey> allUnusedInternalKeys = KeyManager.GetKeys(keyState: null, isInternal: true).Where(x => x.KeyState != KeyState.Used);

                        if (changeAddress is null)
                        {
                            string changeLabel = "ZeroLink Change";
                            IEnumerable <HdPubKey> allChangeKeys = allUnusedInternalKeys.Where(x => x.Label == changeLabel);
                            HdPubKey changeKey = null;

                            KeyManager.AssertLockedInternalKeysIndexed(14);
                            IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys);

                            if (allChangeKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any())                             // Then don't generate new keys, because it'd bloat the wallet.
                            {
                                // Find the first one that we did not try to register in the current session.
                                changeKey = allChangeKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x));
                                // If there is no such a key, then use the oldest.
                                if (changeKey == default)
                                {
                                    changeKey = AccessCache.Where(x => allChangeKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key;
                                }
                                changeKey.SetLabel(changeLabel);
                                changeKey.SetKeyState(KeyState.Locked);
                            }
                            else
                            {
                                changeKey = internalNotCachedLockedKeys.RandomElement();
                                changeKey.SetLabel(changeLabel);
                            }
                            changeAddress = changeKey.GetP2wpkhAddress(Network);
                            AccessCache.AddOrReplace(changeKey, DateTimeOffset.UtcNow);
                        }

                        if (activeAddress is null)
                        {
                            string activeLabel = "ZeroLink Mixed Coin";
                            IEnumerable <HdPubKey> allActiveKeys = allUnusedInternalKeys.Where(x => x.Label == activeLabel);
                            HdPubKey activeKey = null;

                            KeyManager.AssertLockedInternalKeysIndexed(14);
                            IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys);

                            if (allActiveKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any())                             // Then don't generate new keys, because it'd bloat the wallet.
                            {
                                // Find the first one that we did not try to register in the current session.
                                activeKey = allActiveKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x));
                                // If there is no such a key, then use the oldest.
                                if (activeKey == default)
                                {
                                    activeKey = AccessCache.Where(x => allActiveKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key;
                                }
                                activeKey.SetLabel(activeLabel);
                                activeKey.SetKeyState(KeyState.Locked);
                                activeAddress = activeKey.GetP2wpkhAddress(Network);
                            }
                            else
                            {
                                activeKey = internalNotCachedLockedKeys.RandomElement();
                                activeKey.SetLabel(activeLabel);
                            }
                            activeAddress = activeKey.GetP2wpkhAddress(Network);
                            AccessCache.AddOrReplace(activeKey, DateTimeOffset.UtcNow);
                        }
                    }

                    KeyManager.ToFile();

                    var blind = CoordinatorPubKey.Blind(activeAddress.ScriptPubKey.ToBytes());

                    var inputProofs = new List <InputProofModel>();
                    foreach ((uint256 txid, uint index)coinReference in registrableCoins)
                    {
                        SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin is null)
                        {
                            throw new NotSupportedException("This is impossible.");
                        }
                        coin.Secret = coin.Secret ?? KeyManager.GetSecrets(OnePiece, coin.ScriptPubKey).Single();
                        var inputProof = new InputProofModel
                        {
                            Input = coin.GetTxoRef(),
                            Proof = coin.Secret.PrivateKey.SignMessage(ByteHelpers.ToHex(blind.BlindedData))
                        };
                        inputProofs.Add(inputProof);
                    }
                    AliceClient aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blind.BlindedData, inputProofs, CcjHostUri, TorSocks5EndPoint);

                    byte[] unblindedSignature = CoordinatorPubKey.UnblindSignature(aliceClient.BlindedOutputSignature, blind.BlindingFactor);

                    if (!CoordinatorPubKey.Verify(unblindedSignature, activeAddress.ScriptPubKey.ToBytes()))
                    {
                        throw new NotSupportedException("Coordinator did not sign the blinded output properly.");
                    }

                    CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId);
                    if (roundRegistered is 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, aliceClient.RoundId, registeredPeerCount: 1));
                        State.AddOrReplaceRound(roundRegistered);
                    }

                    foreach ((uint256 txid, uint index)coinReference in registrableCoins)
                    {
                        var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin is null)
                        {
                            throw new NotSupportedException("This is impossible.");
                        }
                        roundRegistered.CoinsRegistered.Add(coin);
                        State.RemoveCoinFromWaitingList(coin);
                    }
                    roundRegistered.ActiveOutputAddress = activeAddress;
                    roundRegistered.ChangeOutputAddress = changeAddress;
                    roundRegistered.UnblindedSignature  = unblindedSignature;
                    roundRegistered.AliceClient         = aliceClient;
                }
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }
        private Dictionary <int, WitScript> SignCoinJoin(CcjClientRound ongoingRound, Transaction unsignedCoinJoin)
        {
            TxOut[] myOutputs = unsignedCoinJoin.Outputs
                                .Where(x => x.ScriptPubKey == ongoingRound.Registration.ChangeAddress.ScriptPubKey ||
                                       ongoingRound.Registration.ActiveOutputs.Select(y => y.Address.ScriptPubKey).Contains(x.ScriptPubKey))
                                .ToArray();
            Money amountBack = myOutputs.Sum(y => y.Value);

            // Make sure change is counted.
            Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.Amount);                                                                     // Start with input sum.
            // Do outputs.lenght + 1 in case the server estimated the network fees wrongly due to insufficient data in an edge case.
            Money networkFeesAfterOutputs = ongoingRound.State.FeePerOutputs * (ongoingRound.Registration.AliceClient.RegisteredAddresses.Length + 1); // Use registered addresses here, because network fees are decided at inputregistration.
            Money networkFeesAfterInputs  = ongoingRound.State.FeePerInputs * ongoingRound.Registration.CoinsRegistered.Count();
            Money networkFees             = networkFeesAfterOutputs + networkFeesAfterInputs;

            minAmountBack -= networkFees;             // Minus miner fee.

            IOrderedEnumerable <(Money value, int count)> indistinguishableOutputs = unsignedCoinJoin.GetIndistinguishableOutputs(includeSingle: false).OrderByDescending(x => x.count);

            foreach ((Money value, int count)denomPair in indistinguishableOutputs)
            {
                var mineCount = myOutputs.Count(x => x.Value == denomPair.value);

                Money denomination           = denomPair.value;
                int   anonset                = Math.Min(110, denomPair.count); // https://github.com/zkSNACKs/WalletWasabi/issues/1379
                Money expectedCoordinatorFee = denomination.Percentage(ongoingRound.State.CoordinatorFeePercent * anonset);
                for (int i = 0; i < mineCount; i++)
                {
                    minAmountBack -= expectedCoordinatorFee;                     // Minus expected coordinator fee.
                }
            }

            // If there's no change output then coordinator protection may happened:
            bool gotChange = myOutputs.Select(x => x.ScriptPubKey).Contains(ongoingRound.Registration.ChangeAddress.ScriptPubKey);

            if (!gotChange)
            {
                Money minimumOutputAmount      = Money.Coins(0.0001m);            // If the change would be less than about $1 then add it to the coordinator.
                Money baseDenomination         = indistinguishableOutputs.First().value;
                Money onePercentOfDenomination = baseDenomination.Percentage(1m); // 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 && !amountBack.Almost(minAmountBack, Money.Satoshis(1000)))             // Just in case. Rounding error maybe?
            {
                Money diff = minAmountBack - amountBack;
                throw new NotSupportedException($"Coordinator did not add enough value to our outputs in the coinjoin. Missing: {diff.Satoshi} satoshis.");
            }

            var signedCoinJoin = unsignedCoinJoin.Clone();

            signedCoinJoin.Sign(ongoingRound.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(SaltSoup(), x.ScriptPubKey).Single()).ToArray(), ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin()).ToArray());

            // Old way of signing, which randomly fails! https://github.com/zkSNACKs/WalletWasabi/issues/716#issuecomment-435498906
            // Must be fixed in NBitcoin.
            //var builder = Network.CreateTransactionBuilder();
            //var signedCoinJoin = builder
            //	.ContinueToBuild(unsignedCoinJoin)
            //	.AddKeys(ongoingRound.Registration.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(OnePiece, x.ScriptPubKey).Single()).ToArray())
            //	.AddCoins(ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin()))
            //	.BuildTransaction(true);

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

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

            return(myDic);
        }
Exemple #22
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);
            }
        }