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); } }
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(); } } }
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); }
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."); } }
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(); }
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); }
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); } }
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(); } }
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."); }
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); }
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); } }
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); } }
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); } }
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); }
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); }
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); } }