public async Task DequeueCoinsFromMixAsync(TxoRef[] coins, string reason) { if (coins is null || !coins.Any()) { return; } using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { try { using (await MixLock.LockAsync(cts.Token)) { await DequeueSpentCoinsFromMixNoLockAsync(); await DequeueCoinsFromMixNoLockAsync(coins, reason); } } catch (TaskCanceledException) { await DequeueSpentCoinsFromMixNoLockAsync(); await DequeueCoinsFromMixNoLockAsync(coins, reason); } } }
public async Task DequeueCoinsFromMixAsync(params TxoRef[] coins) { if (coins is null || !coins.Any()) { return; } using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { try { using (await MixLock.LockAsync(cts.Token)) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); await DequeueCoinsFromMixNoLockAsync(coins); } } catch (TaskCanceledException) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); await DequeueCoinsFromMixNoLockAsync(coins); } } }
public async Task <IEnumerable <SmartCoin> > QueueCoinsToMixAsync(string password, params SmartCoin[] coins) { using (await MixLock.LockAsync()) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); var successful = new List <SmartCoin>(); foreach (SmartCoin coin in coins) { if (State.Contains(coin)) { successful.Add(coin); continue; } if (coin.SpentOrCoinJoinInProgress) { continue; } coin.Secret = KeyManager.GetSecrets(password, coin.ScriptPubKey).Single(); OnePiece = OnePiece ?? password; coin.CoinJoinInProgress = true; State.AddCoinToWaitingList(coin); successful.Add(coin); CoinQueued?.Invoke(this, coin); Logger.LogInfo <CcjClient>($"Coin queued: {coin.Index}:{coin.TransactionId}."); } return(successful); } }
public async Task DequeueCoinsFromMixAsync(IEnumerable <SmartCoin> coins, string reason) { if (coins is null || !coins.Any()) { return; } using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { try { using (await MixLock.LockAsync(cts.Token)) { await DequeueSpentCoinsFromMixNoLockAsync(); await DequeueCoinsFromMixNoLockAsync(coins.Select(x => x.GetTxoRef()).ToArray(), reason); } } catch (TaskCanceledException) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray(), reason); await DequeueCoinsFromMixNoLockAsync(coins.Select(x => x.GetTxoRef()).ToArray(), reason); } } }
private async Task TryProcessStatusAsync(IEnumerable <RoundStateResponseBase> states) { states ??= Enumerable.Empty <RoundStateResponseBase>(); if (Interlocked.Read(ref _statusProcessing) == 1) // It's ok to wait for status processing next time. { return; } try { Synchronizer.BlockRequests(); Interlocked.Exchange(ref _statusProcessing, 1); using (await MixLock.LockAsync().ConfigureAwait(false)) { // First, if there's delayed round registration update based on the state. if (DelayedRoundRegistration != null) { ClientRound roundRegistered = State.GetSingleOrDefaultRound(DelayedRoundRegistration.AliceClient.RoundId); roundRegistered.Registration = DelayedRoundRegistration; DelayedRoundRegistration = null; // Do not dispose. } await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false); State.UpdateRoundsByStates(ExposedLinks, states.ToArray()); // If we do not have enough coin queued to register a round, then dequeue all. ClientRound registrableRound = State.GetRegistrableRoundOrDefault(); if (registrableRound is { }) { DequeueReason?reason = null; // If the coordinator increases fees, do not register. Let the users register manually again. if (CoordinatorFeepercentToCheck is { } && registrableRound.State.CoordinatorFeePercent > CoordinatorFeepercentToCheck)
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); }
public async Task DequeueCoinsFromMixAsync(params SmartCoin[] coins) { using (await MixLock.LockAsync()) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); await DequeueCoinsFromMixNoLockAsync(coins.Select(x => (x.TransactionId, x.Index)).ToArray()); } }
public async Task StopAsync() { Synchronizer.ResponseArrived -= Synchronizer_ResponseArrivedAsync; if (IsRunning) { Interlocked.Exchange(ref _running, 2); } Cancel?.Cancel(); while (IsStopping) { Task.Delay(50).GetAwaiter().GetResult(); // DO NOT MAKE IT ASYNC (.NET Core threading brainfart) } Cancel?.Dispose(); using (await MixLock.LockAsync()) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); State.DisposeAllAliceClients(); IEnumerable <TxoRef> allCoins = State.GetAllQueuedCoins(); foreach (var coinReference in allCoins) { try { var coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { continue; // The coin isn't present anymore. Good. This should never happen though. } await DequeueCoinsFromMixNoLockAsync(coin.GetTxoRef()); } catch (Exception ex) { Logger.LogError <CcjClient>("Couldn't dequeue all coins. Some coins will likely be banned from mixing."); if (ex is AggregateException) { var aggrEx = ex as AggregateException; foreach (var innerEx in aggrEx.InnerExceptions) { Logger.LogError <CcjClient>(innerEx); } } else { Logger.LogError <CcjClient>(ex); } } } } }
public async Task StopAsync() { Synchronizer.ResponseArrived -= Synchronizer_ResponseArrivedAsync; Interlocked.CompareExchange(ref _running, 2, 1); // If running, make it stopping. Cancel?.Cancel(); while (Interlocked.CompareExchange(ref _running, 3, 0) == 2) { await Task.Delay(50); } Cancel?.Dispose(); Cancel = null; using (await MixLock.LockAsync()) { await DequeueSpentCoinsFromMixNoLockAsync(); State.DisposeAllAliceClients(); IEnumerable <TxoRef> allCoins = State.GetAllQueuedCoins(); foreach (var coinReference in allCoins) { try { var coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { continue; // The coin isn't present anymore. Good. This should never happen though. } await DequeueCoinsFromMixNoLockAsync(coin.GetTxoRef(), "Stopping Wasabi."); } catch (Exception ex) { Logger.LogError <CcjClient>("Couldn't dequeue all coins. Some coins will likely be banned from mixing."); if (ex is AggregateException) { var aggrEx = ex as AggregateException; foreach (var innerEx in aggrEx.InnerExceptions) { Logger.LogError <CcjClient>(innerEx); } } else { Logger.LogError <CcjClient>(ex); } } } } }
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); } }
public async Task DequeueAllCoinsFromMixAsync(string reason) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); try { using (await MixLock.LockAsync(cts.Token)) { await DequeueAllCoinsFromMixNoLockAsync(reason); } } catch (TaskCanceledException) { await DequeueAllCoinsFromMixNoLockAsync(reason); } }
public async Task DequeueAllCoinsFromMixAsync() { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { try { using (await MixLock.LockAsync(cts.Token)) { await DequeueCoinsFromMixNoLockAsync(State.GetAllQueuedCoins().ToArray()); } } catch (TaskCanceledException) { await DequeueCoinsFromMixNoLockAsync(State.GetAllQueuedCoins().ToArray()); } } }
public async Task DequeueCoinsFromMixAsync(params SmartCoin[] coins) { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { try { using (await MixLock.LockAsync(cts.Token)) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); await DequeueCoinsFromMixNoLockAsync(coins.Select(x => (x.TransactionId, x.Index)).ToArray()); } } catch (TaskCanceledException) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); await DequeueCoinsFromMixNoLockAsync(coins.Select(x => (x.TransactionId, x.Index)).ToArray()); } } }
public void Start() { if (Interlocked.CompareExchange(ref _running, 1, 0) != 0) { return; } // The client is asking for status periodically, randomly between every 0.2 * connConfTimeout and 0.7 * connConfTimeout. // - if the GUI is at the mixer tab -Activate(), DeactivateIfNotMixing(). // - if coins are queued to mix. // The client is asking for status periodically, randomly between every 2 to 7 seconds. // - if it is participating in a mix thats status >= connConf. // The client is triggered only when a status response arrives. The answer to the server is delayed randomly from 0 to 7 seconds. Task.Run(async() => { try { Logger.LogInfo($"{nameof(CoinJoinClient)} is successfully initialized."); while (IsRunning) { try { using (await MixLock.LockAsync().ConfigureAwait(false)) { await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false); // If stop was requested return. if (!IsRunning) { return; } // if mixing >= connConf if (State.GetActivelyMixingRounds().Any()) { int delaySeconds = new Random().Next(2, 7); Synchronizer.MaxRequestIntervalForMixing = TimeSpan.FromSeconds(delaySeconds); } else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || State.GetPassivelyMixingRounds().Any() || State.GetWaitingListCount() > 0) { double rand = double.Parse($"0.{new Random().Next(2, 6)}"); // randomly between every 0.2 * connConfTimeout - 7 and 0.6 * connConfTimeout int delaySeconds = Math.Max(0, (int)((rand * State.GetSmallestRegistrationTimeout()) - 7)); Synchronizer.MaxRequestIntervalForMixing = TimeSpan.FromSeconds(delaySeconds); } else // dormant { Synchronizer.MaxRequestIntervalForMixing = TimeSpan.FromMinutes(3); } } } catch (Exception ex) { Logger.LogError(ex); } finally { try { await Task.Delay(1000, Cancel.Token).ConfigureAwait(false); } catch (TaskCanceledException ex) { Logger.LogTrace(ex); } } } } finally { Interlocked.CompareExchange(ref _running, 3, 2); // If IsStopping, make it stopped. } }); }
public void Start(int minDelayReplySeconds, int maxDelayReplySeconds) { Interlocked.Exchange(ref _running, 1); // At start the client asks for status. // The client is asking for status periodically, randomly between every 0.2 * connConfTimeout and 0.8 * connConfTimeout. // - if the GUI is at the mixer tab -Activate(), DeactivateIfNotMixing(). // - if coins are queued to mix. // The client is asking for status periodically, randomly between every 2 to 7 seconds. // - if it is participating in a mix thats status >= connConf. // The client is triggered only when a status response arrives. The answer to the server is delayed randomly from 0 to 7 seconds. Task.Run(async() => { try { await ProcessStatusAsync(minDelayReplySeconds, maxDelayReplySeconds); Logger.LogInfo <CcjClient>("CcjClient is successfully initialized."); while (IsRunning) { try { int delaySeconds; using (await MixLock.LockAsync()) { await DequeueCoinsFromMixNoLockAsync(State.GetSpentCoins().ToArray()); // If stop was requested return. if (IsRunning == false) { return; } // if mixing >= connConf: delay = new Random().Next(1, 3); if (State.GetActivelyMixingRounds().Any()) { delaySeconds = new Random().Next(1, 3); } else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || State.GetPassivelyMixingRounds().Any() || State.GetWaitingListCount() > 0) { double rand = double.Parse($"0.{new Random().Next(1, 6)}"); // randomly between every 0.1 * connConfTimeout - 7 and 0.6 * connConfTimeout delaySeconds = Math.Max(0, (int)(rand * State.GetSmallestRegistrationTimeout() - 7)); } else { // dormant await Task.Delay(1000); // dormant continue; } } await Task.Delay(TimeSpan.FromSeconds(delaySeconds), Cancel.Token); await ProcessStatusAsync(minDelayReplySeconds, maxDelayReplySeconds); } catch (TaskCanceledException ex) { Logger.LogTrace <CcjClient>(ex); } catch (Exception ex) { Logger.LogError <CcjClient>(ex); } } } finally { if (IsStopping) { Interlocked.Exchange(ref _running, 3); } } }); }
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); } }
public void Start() { Interlocked.Exchange(ref _running, 1); // At start the client asks for status. // The client is asking for status periodically, randomly between every 0.2 * connConfTimeout and 0.8 * connConfTimeout. // - if the GUI is at the mixer tab -Activate(), DeactivateIfNotMixing(). // - if coins are queued to mix. // The client is asking for status periodically, randomly between every 2 to 7 seconds. // - if it is participating in a mix thats status >= connConf. // The client is triggered only when a status response arrives. The answer to the server is delayed randomly from 0 to 7 seconds. Task.Run(async() => { try { await ProcessStatusAsync(); while (IsRunning) { try { // If stop was requested return. if (IsRunning == false) { return; } var inputRegMixing = false; var activelyMixing = false; using (await MixLock.LockAsync()) { // if mixing >= connConf: delay = new Random().Next(2, 7); activelyMixing = Rounds.Any(x => x.AliceUniqueId != null && x.State.Phase >= CcjRoundPhase.ConnectionConfirmation); inputRegMixing = Rounds.Any(x => x.AliceUniqueId != null); } if (activelyMixing) { var delay = new Random().Next(2, 7); await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token); await ProcessStatusAsync(); } else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || inputRegMixing) { double rand = double.Parse($"0.{new Random().Next(2, 8)}"); // randomly between every 0.2 * connConfTimeout and 0.8 * connConfTimeout int delay; using (await MixLock.LockAsync()) { delay = (int)(rand * Rounds.First(x => x.State.Phase == CcjRoundPhase.InputRegistration).State.RegistrationTimeout); } await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token); await ProcessStatusAsync(); } else { await Task.Delay(1000); // dormant } } catch (TaskCanceledException ex) { Logger.LogTrace <CcjClient>(ex); } catch (Exception ex) { Logger.LogError <CcjClient>(ex); } } } finally { if (IsStopping) { Interlocked.Exchange(ref _running, 3); } } }); }
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 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); } }