Add coinjoinstore
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -143,9 +143,17 @@ namespace WalletWasabi.Backend.Controllers
// ToDo: If unconfirmed, then handle the case if CoinJoin. (You can check for if the fee address in the output to decide if it was our CJ or not.)
// Check if unconfirmed.
- if (getTxOutResponse.Confirmations == 0)
+ if (getTxOutResponse.Confirmations <= 0)
{
- return BadRequest("Provided input is unconfirmed.");
+ if (!Coordinator.ContainsCoinJoin(inputProof.Input.Hash))
+ {
+ return BadRequest("Provided input is neither confirmed, nor is from an unconfirmed coinjoin.");
+ }
+ // After 24 unconfirmed cj in the mempool dont't let unconfirmed coinjoin to be registered.
+ if (await Coordinator.IsUnconfirmedCoinJoinLimitReachedAsync())
+ {
+ return BadRequest("Provided input is from an unconfirmed coinjoin, but the maximum number of unconfirmed coinjoins is reached.");
+ }
}
// Check if immature.

MODIFY
WalletWasabi.Backend/Global.cs
WalletWasabi.Backend/Global.cs
@@ -73,7 +73,7 @@ namespace WalletWasabi.Backend
IndexBuilderService = new IndexBuilderService(RpcClient, indexFilePath, utxoSetFilePath);
IndexBuilderService.Synchronize();
- Coordinator = new CcjCoordinator(RpcClient, roundConfig);
+ Coordinator = new CcjCoordinator(RpcClient.Network, Path.Combine(DataDir,nameof(CcjCoordinator)), RpcClient, roundConfig);
await Coordinator.MakeSureTwoRunningRoundsAsync();
if (roundConfig.FilePath != null)

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -1529,7 +1529,7 @@ namespace WalletWasabi.Tests
{
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
string message = await response.Content.ReadAsStringAsync();
- Assert.Equal("\"Provided input is unconfirmed.\"", message);
+ Assert.Equal("\"Provided input is neither confirmed, nor is from an unconfirmed coinjoin.\"", message);
}
var blocks = await rpc.GenerateAsync(1);

MODIFY
WalletWasabi/Services/CcjCoordinator.cs
WalletWasabi/Services/CcjCoordinator.cs
@@ -3,6 +3,7 @@ using NBitcoin.RPC;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
@@ -18,17 +19,70 @@ namespace WalletWasabi.Services
private List<CcjRound> Rounds { get; }
private AsyncLock RoundsListLock { get; }
+ private List<uint256> UnconfirmedCoinJoins { get; }
+ private List<uint256> CoinJoins { get; }
+ public string CoinJoinsFilePath => Path.Combine(FolderPath, $"CoinJoins{Network}.txt");
+ private AsyncLock CoinJoinsLock { get; }
+
public RPCClient RpcClient { get; }
public CcjRoundConfig RoundConfig { get; private set; }
- public CcjCoordinator(RPCClient rpc, CcjRoundConfig roundConfig)
+ public Network Network { get; }
+ public string FolderPath { get; }
+
+ public CcjCoordinator(Network network, string folderPath, RPCClient rpc, CcjRoundConfig roundConfig)
{
+ Network = Guard.NotNull(nameof(network), network);
+ FolderPath = Guard.NotNullOrEmptyOrWhitespace(nameof(folderPath), folderPath, trim: true);
RpcClient = Guard.NotNull(nameof(rpc), rpc);
RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig);
Rounds = new List<CcjRound>();
RoundsListLock = new AsyncLock();
+
+ CoinJoins = new List<uint256>();
+ UnconfirmedCoinJoins = new List<uint256>();
+ CoinJoinsLock = new AsyncLock();
+
+ Directory.CreateDirectory(FolderPath);
+ if(File.Exists(CoinJoinsFilePath))
+ {
+ try
+ {
+ var toRemove = new List<string>();
+ string[] allLines = File.ReadAllLines(CoinJoinsFilePath);
+ foreach (string line in allLines)
+ {
+ uint256 txHash = new uint256(line);
+ CoinJoins.Add(txHash);
+ RPCResponse getRawTransactionResponse = RpcClient.SendCommand(RPCOperations.getrawtransaction, txHash.ToString(), true);
+ if (string.IsNullOrWhiteSpace(getRawTransactionResponse?.ResultString))
+ {
+ toRemove.Add(txHash.ToString());
+ }
+ else
+ {
+ CoinJoins.Add(txHash);
+ if (getRawTransactionResponse.Result.Value<int>("confirmations") <= 0)
+ {
+ UnconfirmedCoinJoins.Add(txHash);
+ }
+ }
+ }
+
+ 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<CcjCoordinator>($"CoinJoins file got corrupted. Deleting {CoinJoinsFilePath}. {ex.GetType()}: {ex.Message}");
+ File.Delete(CoinJoinsFilePath);
+ }
+ }
}
public void UpdateRoundConfig(CcjRoundConfig roundConfig)
@@ -66,6 +120,18 @@ namespace WalletWasabi.Services
private async void Round_StatusChangedAsync(object sender, CcjRoundStatus status)
{
var round = sender as CcjRound;
+
+ // If success save the coinjoin.
+ if (status == CcjRoundStatus.Succeded)
+ {
+ using (await CoinJoinsLock.LockAsync())
+ {
+ uint256 coinJoinHash = round.SignedCoinJoin.GetHash();
+ CoinJoins.Add(coinJoinHash);
+ await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() });
+ }
+ }
+
// If finished start a new round.
if (status == CcjRoundStatus.Failed || status == CcjRoundStatus.Succeded)
{
@@ -163,6 +229,46 @@ namespace WalletWasabi.Services
}
}
+ public bool ContainsCoinJoin(uint256 hash)
+ {
+ using (CoinJoinsLock.Lock())
+ {
+ return CoinJoins.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;
+ }
+ }
+
#region IDisposable Support
private volatile bool _disposedValue = false; // To detect redundant calls

Add coinjoinstore