public async Task <IEnumerable <uint256> > GetUnconfirmedCoinJoinsAsync() { using (await CoinJoinsLock.LockAsync()) { return(UnconfirmedCoinJoins.ToArray()); } }
public async Task ProcessConfirmedTransactionAsync(Transaction tx) { // This should not be needed until we would only accept unconfirmed CJ outputs an no other unconf outs. But it'll be more bulletproof for future extensions. // Turns out you shouldn't accept RBF at all never. (See below.) // https://github.com/zkSNACKs/WalletWasabi/issues/145 // if it spends a banned output AND it's not CJ output // ban all the outputs of the transaction tx.PrecomputeHash(false, true); UnconfirmedCoinJoins.Remove(tx.GetHash()); // Locked outside. if (RoundConfig.DosSeverity <= 1) { return; } foreach (TxIn input in tx.Inputs) { OutPoint prevOut = input.PrevOut; // if coin is not banned var foundElem = await UtxoReferee.TryGetBannedAsync(prevOut, notedToo : true).ConfigureAwait(false); if (foundElem is { })
public async Task <bool> ContainsUnconfirmedCoinJoinAsync(uint256 hash) { using (await CoinJoinsLock.LockAsync().ConfigureAwait(false)) { return(UnconfirmedCoinJoins.Contains(hash)); } }
public async Task <bool> IsUnconfirmedCoinJoinLimitReachedAsync() { using (await CoinJoinsLock.LockAsync()) { if (UnconfirmedCoinJoins.Count() < 24) { return(false); } else { foreach (var cjHash in UnconfirmedCoinJoins) { RPCResponse getRawTransactionResponse = await RpcClient.SendCommandAsync(RPCOperations.getrawtransaction, cjHash.ToString(), true); // if failed remove from everywhere (should not happen normally) if (string.IsNullOrWhiteSpace(getRawTransactionResponse?.ResultString)) { UnconfirmedCoinJoins.Remove(cjHash); CoinJoins.Remove(cjHash); await File.WriteAllLinesAsync(CoinJoinsFilePath, CoinJoins.Select(x => x.ToString())); } // if confirmed remove only from unconfirmed if (getRawTransactionResponse.Result.Value <int>("confirmations") > 0) { UnconfirmedCoinJoins.Remove(cjHash); } } } return(UnconfirmedCoinJoins.Count() >= 24); } }
public async Task <bool> IsUnconfirmedCoinJoinLimitReachedAsync() { using (await CoinJoinsLock.LockAsync()) { if (UnconfirmedCoinJoins.Count < 24) { return(false); } foreach (var cjHash in UnconfirmedCoinJoins.ToArray()) { try { var txInfo = await RpcClient.GetRawTransactionInfoAsync(cjHash); // if confirmed remove only from unconfirmed if (txInfo.Confirmations > 0) { UnconfirmedCoinJoins.Remove(cjHash); } } catch (Exception ex) { // If aborted remove from everywhere (should not happen normally). UnconfirmedCoinJoins.Remove(cjHash); CoinJoins.Remove(cjHash); await File.WriteAllLinesAsync(CoinJoinsFilePath, CoinJoins.Select(x => x.ToString())); Logger.LogWarning <CcjCoordinator>(ex); } } return(UnconfirmedCoinJoins.Count >= 24); } }
public async Task ProcessConfirmedTransactionAsync(Transaction tx) { // This should not be needed until we would only accept unconfirmed CJ outputs an no other unconf outs. But it'll be more bulletproof for future extensions. // Turns out you shouldn't accept RBF at all never. (See below.) // https://github.com/zkSNACKs/WalletWasabi/issues/145 // if it spends a banned output AND it's not CJ output // ban all the outputs of the transaction tx.PrecomputeHash(false, true); UnconfirmedCoinJoins.Remove(tx.GetHash()); // Locked outside. if (RoundConfig.DosSeverity <= 1) { return; } foreach (TxIn input in tx.Inputs) { OutPoint prevOut = input.PrevOut; // if coin is not banned var foundElem = await UtxoReferee.TryGetBannedAsync(prevOut, notedToo : true).ConfigureAwait(false); if (foundElem != null) { if (!AnyRunningRoundContainsInput(prevOut, out _)) { int newSeverity = foundElem.Severity + 1; await UtxoReferee.UnbanAsync(prevOut).ConfigureAwait(false); // since it's not an UTXO anymore if (RoundConfig.DosSeverity >= newSeverity) { var txCoins = tx.Outputs.AsIndexedOutputs().Select(x => x.ToCoin().Outpoint); await UtxoReferee.BanUtxosAsync(newSeverity, foundElem.TimeOfBan, forceNoted : foundElem.IsNoted, foundElem.BannedForRound, txCoins.ToArray()).ConfigureAwait(false); } } } } }
private async void Round_StatusChangedAsync(object sender, CoordinatorRoundStatus status) { try { var round = sender as CoordinatorRound; Money feePerInputs = null; Money feePerOutputs = null; // If success save the coinjoin. if (status == CoordinatorRoundStatus.Succeded) { uint256[] mempoolHashes = null; try { mempoolHashes = await RpcClient.GetRawMempoolAsync().ConfigureAwait(false); } catch (Exception ex) { Logger.LogError(ex); } using (await CoinJoinsLock.LockAsync().ConfigureAwait(false)) { if (mempoolHashes is { }) { var fallOuts = UnconfirmedCoinJoins.Where(x => !mempoolHashes.Contains(x)); CoinJoins.RemoveAll(x => fallOuts.Contains(x)); UnconfirmedCoinJoins.RemoveAll(x => fallOuts.Contains(x)); } uint256 coinJoinHash = round.CoinJoin.GetHash(); CoinJoins.Add(coinJoinHash); UnconfirmedCoinJoins.Add(coinJoinHash); LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow; await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() }).ConfigureAwait(false); // When a round succeeded, adjust the denomination as to users still be able to register with the latest round's active output amount. IEnumerable <(Money value, int count)> outputs = round.CoinJoin.GetIndistinguishableOutputs(includeSingle: true); var bestOutput = outputs.OrderByDescending(x => x.count).FirstOrDefault(); if (bestOutput != default) { Money activeOutputAmount = bestOutput.value; int currentConfirmationTarget = await AdjustConfirmationTargetAsync(lockCoinJoins : false).ConfigureAwait(false); var fees = await CoordinatorRound.CalculateFeesAsync(RpcClient, currentConfirmationTarget).ConfigureAwait(false); feePerInputs = fees.feePerInputs; feePerOutputs = fees.feePerOutputs; Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + (2 * feePerOutputs)); if (newDenominationToGetInWithactiveOutputs < RoundConfig.Denomination) { if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m)) { RoundConfig.Denomination = newDenominationToGetInWithactiveOutputs; RoundConfig.ToFile(); } } } }
private async void Round_StatusChangedAsync(object sender, CoordinatorRoundStatus status) { try { var round = sender as CoordinatorRound; Money feePerInputs = null; Money feePerOutputs = null; // If success save the coinjoin. if (status == CoordinatorRoundStatus.Succeded) { using (await CoinJoinsLock.LockAsync().ConfigureAwait(false)) { uint256 coinJoinHash = round.CoinJoin.GetHash(); CoinJoins.Add(coinJoinHash); UnconfirmedCoinJoins.Add(coinJoinHash); LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow; await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() }).ConfigureAwait(false); // When a round succeeded, adjust the denomination as to users still be able to register with the latest round's active output amount. IEnumerable <(Money value, int count)> outputs = round.CoinJoin.GetIndistinguishableOutputs(includeSingle: true); var bestOutput = outputs.OrderByDescending(x => x.count).FirstOrDefault(); if (bestOutput != default) { Money activeOutputAmount = bestOutput.value; int currentConfirmationTarget = await AdjustConfirmationTargetAsync(lockCoinJoins : false).ConfigureAwait(false); var fees = await CoordinatorRound.CalculateFeesAsync(RpcClient, currentConfirmationTarget).ConfigureAwait(false); feePerInputs = fees.feePerInputs; feePerOutputs = fees.feePerOutputs; Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + (2 * feePerOutputs)); if (newDenominationToGetInWithactiveOutputs < RoundConfig.Denomination) { if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m)) { RoundConfig.Denomination = newDenominationToGetInWithactiveOutputs; RoundConfig.ToFile(); } } } } } // If aborted in signing phase, then ban Alices that did not sign. if (status == CoordinatorRoundStatus.Aborted && round.Phase == RoundPhase.Signing) { IEnumerable <Alice> alicesDidntSign = round.GetAlicesByNot(AliceState.SignedCoinJoin, syncLock: false); CoordinatorRound nextRound = GetCurrentInputRegisterableRoundOrDefault(syncLock: false); if (nextRound != null) { int nextRoundAlicesCount = nextRound.CountAlices(syncLock: false); var alicesSignedCount = round.AnonymitySet - alicesDidntSign.Count(); // New round's anonset should be the number of alices that signed in this round. // Except if the number of alices in the next round is already larger. var newAnonymitySet = Math.Max(alicesSignedCount, nextRoundAlicesCount); // But it cannot be larger than the current anonset of that round. newAnonymitySet = Math.Min(newAnonymitySet, nextRound.AnonymitySet); // Only change the anonymity set of the next round if new anonset does not equal and newanonset is larger than 1. if (nextRound.AnonymitySet != newAnonymitySet && newAnonymitySet > 1) { nextRound.UpdateAnonymitySet(newAnonymitySet, syncLock: false); if (nextRoundAlicesCount >= nextRound.AnonymitySet) { // Progress to the next phase, which will be OutputRegistration await nextRound.ExecuteNextPhaseAsync(RoundPhase.ConnectionConfirmation).ConfigureAwait(false); } } } foreach (Alice alice in alicesDidntSign) // Because the event sometimes is raised from inside the lock. { // If it is from any coinjoin, then do not ban. IEnumerable <OutPoint> utxosToBan = alice.Inputs.Select(x => x.Outpoint); await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted : false, round.RoundId, utxosToBan.ToArray()).ConfigureAwait(false); } } // If finished start a new round. if (status == CoordinatorRoundStatus.Aborted || status == CoordinatorRoundStatus.Succeded) { round.StatusChanged -= Round_StatusChangedAsync; round.CoinJoinBroadcasted -= Round_CoinJoinBroadcasted; await MakeSureTwoRunningRoundsAsync(feePerInputs, feePerOutputs).ConfigureAwait(false); } } catch (Exception ex) { Logger.LogWarning(ex); } }
private volatile bool _disposedValue = false; // To detect redundant calls public Coordinator(Network network, BlockNotifier blockNotifier, string folderPath, IRPCClient rpc, CoordinatorRoundConfig roundConfig) { Network = Guard.NotNull(nameof(network), network); BlockNotifier = Guard.NotNull(nameof(blockNotifier), blockNotifier); FolderPath = Guard.NotNullOrEmptyOrWhitespace(nameof(folderPath), folderPath, trim: true); RpcClient = Guard.NotNull(nameof(rpc), rpc); RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig); Rounds = new List <CoordinatorRound>(); RoundsListLock = new AsyncLock(); LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow; Directory.CreateDirectory(FolderPath); UtxoReferee = new UtxoReferee(Network, FolderPath, RpcClient, RoundConfig); if (File.Exists(CoinJoinsFilePath)) { try { var getTxTasks = new List <(Task <Transaction> txTask, string line)>(); var batch = RpcClient.PrepareBatch(); var toRemove = new List <string>(); string[] allLines = File.ReadAllLines(CoinJoinsFilePath); foreach (string line in allLines) { try { getTxTasks.Add((batch.GetRawTransactionAsync(uint256.Parse(line)), line)); } catch (Exception ex) { toRemove.Add(line); var logEntry = ex is RPCException rpce && rpce.RPCCode == RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY ? $"CoinJoins file contains invalid transaction ID {line}" : $"CoinJoins file got corrupted. Deleting offending line \"{line.Substring(0, 20)}\"."; Logger.LogWarning($"{logEntry}. {ex.GetType()}: {ex.Message}"); } } batch.SendBatchAsync().GetAwaiter().GetResult(); foreach (var(txTask, line) in getTxTasks) { try { var tx = txTask.GetAwaiter().GetResult(); CoinJoins.Add(tx.GetHash()); } catch (Exception ex) { toRemove.Add(line); var logEntry = ex is RPCException rpce && rpce.RPCCode == RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY ? $"CoinJoins file contains invalid transaction ID {line}" : $"CoinJoins file got corrupted. Deleting offending line \"{line.Substring(0, 20)}\"."; Logger.LogWarning($"{logEntry}. {ex.GetType()}: {ex.Message}"); } } if (toRemove.Count != 0) // a little performance boost, it'll be empty almost always { var newAllLines = allLines.Where(x => !toRemove.Contains(x)); File.WriteAllLines(CoinJoinsFilePath, newAllLines); } } catch (Exception ex) { Logger.LogWarning($"CoinJoins file got corrupted. Deleting {CoinJoinsFilePath}. {ex.GetType()}: {ex.Message}"); File.Delete(CoinJoinsFilePath); } uint256[] mempoolHashes = RpcClient.GetRawMempoolAsync().GetAwaiter().GetResult(); UnconfirmedCoinJoins.AddRange(CoinJoins.Intersect(mempoolHashes)); } try { string roundCountFilePath = Path.Combine(folderPath, "RoundCount.txt"); if (File.Exists(roundCountFilePath)) { string roundCount = File.ReadAllText(roundCountFilePath); CoordinatorRound.RoundCount = long.Parse(roundCount); } else { // First time initializes (so the first constructor will increment it and we'll start from 1.) CoordinatorRound.RoundCount = 0; } } catch (Exception ex) { CoordinatorRound.RoundCount = 0; Logger.LogInfo($"{nameof(CoordinatorRound.RoundCount)} file was corrupt. Resetting to 0."); Logger.LogDebug(ex); } BlockNotifier.OnBlock += BlockNotifier_OnBlockAsync; }