Try implementing noting
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -156,15 +156,10 @@ namespace WalletWasabi.Backend.Controllers
}
OutPoint outpoint = inputProof.Input.ToOutPoint();
- if (Coordinator.UtxoReferee.BannedUtxos.TryGetValue(outpoint, out (int severity, DateTimeOffset timeOfBan) bannedElem))
+ var bannedElem = await Coordinator.UtxoReferee.TryGetBannedAsync(outpoint, notedToo: false);
+ if (bannedElem != null)
{
- int maxBan = (int)TimeSpan.FromHours((long)Global.RoundConfig.DosDurationHours).TotalMinutes;
- int banLeft = maxBan - (int)(DateTimeOffset.UtcNow - bannedElem.timeOfBan).TotalMinutes;
- if (banLeft > 0)
- {
- return BadRequest($"Input is banned from participation for {banLeft} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.");
- }
- await Coordinator.UtxoReferee.UnbanAsync(outpoint);
+ return BadRequest($"Input is banned from participation for {(int)bannedElem.Value.bannedRemaining.TotalMinutes} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.");
}
GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(inputProof.Input.TransactionId, (int)inputProof.Input.Index, includeMempool: true);
@@ -339,7 +334,7 @@ namespace WalletWasabi.Backend.Controllers
if (alicesToBan.Any())
{
- await Coordinator.UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, alicesToBan.SelectMany(x => x.Inputs).Select(y => y.Outpoint).ToArray());
+ await Coordinator.UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted: false, round.RoundId, alicesToBan.SelectMany(x => x.Inputs).Select(y => y.Outpoint).ToArray());
}
int aliceCountAfterConnectionConfirmationTimeout = round.CountAlices();

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -1604,7 +1604,7 @@ namespace WalletWasabi.Tests
decimal coordinatorFeePercent = 0.2m;
int anonymitySet = 2;
int connectionConfirmationTimeout = 50;
- var roundConfig = new CcjRoundConfig(denomination, 2, coordinatorFeePercent, anonymitySet, 100, connectionConfirmationTimeout, 50, 50, 2, 24);
+ var roundConfig = new CcjRoundConfig(denomination, 2, coordinatorFeePercent, anonymitySet, 100, connectionConfirmationTimeout, 50, 50, 2, 24, false);
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
@@ -1817,7 +1817,7 @@ namespace WalletWasabi.Tests
var spendingTx = network.Consensus.ConsensusFactory.CreateTransaction();
var bannedCoin = inputsRequest.Inputs.First().Input;
var utxos = coordinator.UtxoReferee;
- Assert.Contains(utxos.BannedUtxos, x => x.Key == bannedCoin);
+ Assert.NotNull(await utxos.TryGetBannedAsync(bannedCoin.ToOutPoint(), false));
spendingTx.Inputs.Add(new TxIn(bannedCoin.ToOutPoint()));
spendingTx.Outputs.Add(new TxOut(Money.Coins(1), new Key().PubKey.GetSegwitAddress(network)));
var fakeBlockWithSpendingBannedCoins = network.Consensus.ConsensusFactory.CreateBlock();
@@ -1825,8 +1825,8 @@ namespace WalletWasabi.Tests
await coordinator.ProcessBlockAsync(fakeBlockWithSpendingBannedCoins);
- Assert.Contains(utxos.BannedUtxos, x => x.Key == new OutPoint(spendingTx.GetHash(), 0));
- Assert.DoesNotContain(utxos.BannedUtxos, x => x.Key == bannedCoin);
+ Assert.NotNull(await utxos.TryGetBannedAsync(new OutPoint(spendingTx.GetHash(), 0), false));
+ Assert.Null(await utxos.TryGetBannedAsync(bannedCoin.ToOutPoint(), false));
states = await satoshiClient.GetAllRoundStatesAsync();
foreach (var rs in states.Where(x => x.Phase == CcjRoundPhase.InputRegistration))
@@ -2073,9 +2073,10 @@ namespace WalletWasabi.Tests
decimal coordinatorFeePercent = 0.1m;
int anonymitySet = 3;
int connectionConfirmationTimeout = 120;
- var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 1, 1, 1, 24);
+ var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 1, 1, 1, 24, true);
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
+
await rpc.GenerateAsync(3); // So to make sure we have enough money.
Uri baseUri = new Uri(RegTestFixture.BackendEndPoint);
@@ -2179,18 +2180,87 @@ namespace WalletWasabi.Tests
}
}
- await Task.Delay(3000);
+ using (var satoshiClient = new SatoshiClient(baseUri))
+ {
+ var times = 0;
+ while (!(await satoshiClient.GetAllRoundStatesAsync()).All(x => x.Phase == CcjRoundPhase.InputRegistration))
+ {
+ await Task.Delay(100);
+ if (times > 50) // 5 sec, 3 should be enough
+ {
+ throw new TimeoutException("Not all rouns were in InputRegistration.");
+ }
+ times++;
+ }
+ }
+
+ int bannedCount = coordinator.UtxoReferee.CountBanned(false);
+ Assert.Equal(0, bannedCount);
+
+ aliceClients.Clear();
+ foreach (var user in inputRegistrationUsers)
+ {
+ aliceClients.Add(AliceClient.CreateNewAsync(network, user.changeOutputAddress, user.blinded.blindedData, user.inputProofModels, baseUri));
+ }
+
+ roundId = 0;
+ users = new List<((BigInteger blindingFactor, byte[] blindedData) blinded, BitcoinAddress activeOutputAddress, BitcoinAddress changeOutputAddress, IEnumerable<InputProofModel> inputProofModels, List<(Key key, BitcoinWitPubKeyAddress address, uint256 txHash, Transaction tx, OutPoint input)> userInputData, AliceClient aliceClient, byte[] unblindedSignature)>();
+ for (int i = 0; i < inputRegistrationUsers.Count; i++)
+ {
+ var user = inputRegistrationUsers[i];
+ var request = aliceClients[i];
+
+ var aliceClient = await request;
+ if (roundId == 0)
+ {
+ roundId = aliceClient.RoundId;
+ }
+ else
+ {
+ Assert.Equal(roundId, aliceClient.RoundId);
+ }
+ // Because it's valuetuple.
+ users.Add((user.blinded, user.activeOutputAddress, user.changeOutputAddress, user.inputProofModels, user.userInputData, aliceClient, blindingKey.PubKey.UnblindSignature(aliceClient.BlindedOutputSignature, user.blinded.blindingFactor)));
+ }
+
+ Assert.Equal(users.Count, roundConfig.AnonymitySet);
+
+ confirmationRequests = new List<Task<string>>();
+
+ foreach (var user in users)
+ {
+ confirmationRequests.Add(user.aliceClient.PostConfirmationAsync());
+ }
+
+ roundHash = null;
+ foreach (var request in confirmationRequests)
+ {
+ if (roundHash is null)
+ {
+ roundHash = await request;
+ }
+ else
+ {
+ Assert.Equal(roundHash, await request);
+ }
+ }
using (var satoshiClient = new SatoshiClient(baseUri))
{
- IEnumerable<CcjRunningRoundState> states = await satoshiClient.GetAllRoundStatesAsync();
- foreach (CcjRoundPhase phase in states.Select(x => x.Phase))
+ var times = 0;
+ while (!(await satoshiClient.GetAllRoundStatesAsync()).All(x => x.Phase == CcjRoundPhase.InputRegistration))
{
- Assert.Equal(CcjRoundPhase.InputRegistration, phase);
+ await Task.Delay(100);
+ if (times > 50) // 5 sec, 3 should be enough
+ {
+ throw new TimeoutException("Not all rouns were in InputRegistration.");
+ }
+ times++;
}
}
- Assert.True(coordinator.UtxoReferee.BannedUtxos.Count >= roundConfig.AnonymitySet);
+ bannedCount = coordinator.UtxoReferee.CountBanned(false);
+ Assert.True(bannedCount >= roundConfig.AnonymitySet);
foreach (var aliceClient in aliceClients)
{
@@ -2209,7 +2279,7 @@ namespace WalletWasabi.Tests
decimal coordinatorFeePercent = 0.003m;
int anonymitySet = 100;
int connectionConfirmationTimeout = 120;
- var roundConfig = new CcjRoundConfig(denomination, 144, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24);
+ var roundConfig = new CcjRoundConfig(denomination, 144, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24, true);
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
await rpc.GenerateAsync(100); // So to make sure we have enough money.
@@ -2416,7 +2486,7 @@ namespace WalletWasabi.Tests
decimal coordinatorFeePercent = 0.1m;
int anonymitySet = 2;
int connectionConfirmationTimeout = 14;
- var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24);
+ var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24, true);
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
await rpc.GenerateAsync(3); // So to make sure we have enough money.
@@ -2516,7 +2586,7 @@ namespace WalletWasabi.Tests
// Make sure if times out, it tries again.
connectionConfirmationTimeout = 1;
- roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24);
+ roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24, true);
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
Assert.NotEmpty(chaumianClient1.State.GetAllQueuedCoins());
@@ -2561,7 +2631,7 @@ namespace WalletWasabi.Tests
decimal coordinatorFeePercent = 0.1m;
int anonymitySet = 2;
int connectionConfirmationTimeout = 14;
- var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24);
+ var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24, true);
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");
await rpc.GenerateAsync(3); // So to make sure we have enough money.
@@ -2658,7 +2728,7 @@ namespace WalletWasabi.Tests
decimal coordinatorFeePercent = 0.1m;
int anonymitySet = 2;
int connectionConfirmationTimeout = 14;
- var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24);
+ var roundConfig = new CcjRoundConfig(denomination, 140, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 50, 50, 1, 24, true);
coordinator.UpdateRoundConfig(roundConfig);
coordinator.AbortAllRoundsInInputRegistration(nameof(RegTests), "");

MODIFY
WalletWasabi.Tests/XunitConfiguration/RegTestFixture.cs
WalletWasabi.Tests/XunitConfiguration/RegTestFixture.cs
@@ -33,7 +33,7 @@ namespace WalletWasabi.Tests.XunitConfiguration
var config = new Config(rpc.Network, authString[0], authString[1]);
- var roundConfig = new CcjRoundConfig(Money.Coins(0.1m), 144, 0.1m, 100, 120, 60, 60, 60, 1, 24);
+ var roundConfig = new CcjRoundConfig(Money.Coins(0.1m), 144, 0.1m, 100, 120, 60, 60, 60, 1, 24, true);
Global.InitializeAsync(config, roundConfig, rpc).GetAwaiter().GetResult();

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/CcjRound.cs
WalletWasabi/Models/ChaumianCoinJoin/CcjRound.cs
@@ -463,7 +463,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
if (inputsToBan.Any())
{
- await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, inputsToBan.ToArray());
+ await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted: false, RoundId, inputsToBan.ToArray());
}
RemoveAlicesBy(alicesToBan1.Select(x => x.UniqueId).Concat(alicesToBan2.Select(y => y.UniqueId)).Distinct().ToArray());
@@ -506,7 +506,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
}
if (outpointsToBan.Any())
{
- await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, outpointsToBan.ToArray());
+ await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted: false, RoundId, outpointsToBan.ToArray());
}
Abort(nameof(CcjRound), "Not all Alices signed.");
}

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/CcjRoundConfig.cs
WalletWasabi/Models/ChaumianCoinJoin/CcjRoundConfig.cs
@@ -48,6 +48,9 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
[JsonProperty(PropertyName = "DosDurationHours")]
public long? DosDurationHours { get; internal set; }
+ [JsonProperty(PropertyName = "DosNoteBeforeBan")]
+ public bool? DosNoteBeforeBan { get; internal set; }
+
public CcjRoundConfig()
{
}
@@ -57,7 +60,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
SetFilePath(filePath);
}
- public CcjRoundConfig(Money denomination, int? confirmationTarget, decimal? coordinatorFeePercent, int? anonymitySet, long? inputRegistrationTimeout, long? connectionConfirmationTimeout, long? outputRegistrationTimeout, long? signingTimeout, int? dosSeverity, long? dosDurationHours)
+ public CcjRoundConfig(Money denomination, int? confirmationTarget, decimal? coordinatorFeePercent, int? anonymitySet, long? inputRegistrationTimeout, long? connectionConfirmationTimeout, long? outputRegistrationTimeout, long? signingTimeout, int? dosSeverity, long? dosDurationHours, bool dosNoteBeforeBan)
{
FilePath = null;
Denomination = Guard.NotNull(nameof(denomination), denomination);
@@ -70,6 +73,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
SigningTimeout = Guard.NotNull(nameof(signingTimeout), signingTimeout);
DosSeverity = Guard.NotNull(nameof(dosSeverity), dosSeverity);
DosDurationHours = Guard.NotNull(nameof(dosDurationHours), dosDurationHours);
+ DosNoteBeforeBan = Guard.NotNull(nameof(dosNoteBeforeBan), dosNoteBeforeBan);
}
/// <inheritdoc />
@@ -98,6 +102,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
SigningTimeout = 60;
DosSeverity = 1;
DosDurationHours = 730; // 1 month
+ DosNoteBeforeBan = true;
if (!File.Exists(FilePath))
{
@@ -126,6 +131,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
SigningTimeout = config.SigningTimeout ?? SigningTimeout;
DosSeverity = config.DosSeverity ?? DosSeverity;
DosDurationHours = config.DosDurationHours ?? DosDurationHours;
+ DosNoteBeforeBan = config.DosNoteBeforeBan ?? DosNoteBeforeBan;
}
/// <inheritdoc />
@@ -181,6 +187,10 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
{
return true;
}
+ if (DosNoteBeforeBan != config.DosNoteBeforeBan)
+ {
+ return true;
+ }
return false;
}

MODIFY
WalletWasabi/Services/CcjCoordinator.cs
WalletWasabi/Services/CcjCoordinator.cs
@@ -153,17 +153,18 @@ namespace WalletWasabi.Services
OutPoint prevOut = input.PrevOut;
// if coin is not banned
- if (UtxoReferee.BannedUtxos.TryGetValue(prevOut, out (int severity, DateTimeOffset timeOfBan) found))
+ var foundElem = await UtxoReferee.TryGetBannedAsync(prevOut, notedToo: true);
+ if (foundElem != null)
{
if (!AnyRunningRoundContainsInput(prevOut, out _))
{
- int newSeverity = found.severity + 1;
+ int newSeverity = foundElem.Value.severity + 1;
await UtxoReferee.UnbanAsync(prevOut); // 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, found.timeOfBan, txCoins.ToArray());
+ await UtxoReferee.BanUtxosAsync(newSeverity, foundElem.Value.timeOfBan, forceNoted: foundElem.Value.isNoted, foundElem.Value.bannedForRound, txCoins.ToArray());
}
}
}
@@ -233,7 +234,7 @@ namespace WalletWasabi.Services
{
// If its from any coinjoin, then don't ban.
IEnumerable<OutPoint> utxosToBan = alice.Inputs.Select(x => x.Outpoint);
- await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, utxosToBan.ToArray());
+ await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted: false, round.RoundId, utxosToBan.ToArray());
}
}

MODIFY
WalletWasabi/Services/UtxoReferee.cs
WalletWasabi/Services/UtxoReferee.cs
@@ -16,9 +16,9 @@ namespace WalletWasabi.Services
public class UtxoReferee
{
/// <summary>
- /// Key: banned utxo, Value: severity level, time of ban
+ /// Key: banned utxo, Value: severity level, time of ban, if it's only in noted status, which round it disrupted
/// </summary>
- public ConcurrentDictionary<OutPoint, (int severity, DateTimeOffset timeOfBan)> BannedUtxos { get; }
+ private ConcurrentDictionary<OutPoint, (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)> BannedUtxos { get; }
public string BannedUtxosFilePath => Path.Combine(FolderPath, $"BannedUtxos{Network}.txt");
@@ -36,7 +36,7 @@ namespace WalletWasabi.Services
RpcClient = Guard.NotNull(nameof(rpc), rpc);
RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig);
- BannedUtxos = new ConcurrentDictionary<OutPoint, (int severity, DateTimeOffset timeOfBan)>();
+ BannedUtxos = new ConcurrentDictionary<OutPoint, (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)>();
Directory.CreateDirectory(FolderPath);
@@ -49,6 +49,8 @@ namespace WalletWasabi.Services
foreach (string line in allLines)
{
var parts = line.Split(':');
+ var isNoted = parts.Length <= 4 ? false : bool.Parse(parts[4]);
+ var bannedForRound = parts.Length <= 4 ? 0 : long.Parse(parts[5]);
var utxo = new OutPoint(new uint256(parts[3]), int.Parse(parts[2]));
var severity = int.Parse(parts[1]);
var timeOfBan = DateTimeOffset.Parse(parts[0], CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
@@ -62,7 +64,7 @@ namespace WalletWasabi.Services
}
else
{
- BannedUtxos.TryAdd(utxo, (severity, timeOfBan));
+ BannedUtxos.TryAdd(utxo, (severity, timeOfBan, isNoted, bannedForRound));
}
}
@@ -86,7 +88,7 @@ namespace WalletWasabi.Services
}
}
- public async Task BanUtxosAsync(int severity, DateTimeOffset timeOfBan, params OutPoint[] toBan)
+ public async Task BanUtxosAsync(int severity, DateTimeOffset timeOfBan, bool forceNoted, long bannedForRound, params OutPoint[] toBan)
{
if (RoundConfig.DosSeverity == 0)
{
@@ -94,30 +96,137 @@ namespace WalletWasabi.Services
}
var lines = new List<string>();
+ var append = false;
+ var updatedOrAdded = false;
foreach (var utxo in toBan)
{
- if (BannedUtxos.TryAdd(utxo, (severity, timeOfBan)))
+ var isNoted = true;
+ if (forceNoted)
{
- string line = $"{timeOfBan.ToString(CultureInfo.InvariantCulture)}:{severity}:{utxo.N}:{utxo.Hash}";
+ isNoted = true;
+ }
+ else
+ {
+ if (RoundConfig.DosNoteBeforeBan.Value)
+ {
+ if (BannedUtxos.TryGetValue(utxo, out (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound) foundElem))
+ {
+ if (foundElem.bannedForRound != bannedForRound)
+ {
+ isNoted = false;
+ }
+ else
+ {
+ isNoted = foundElem.isNoted;
+ }
+ }
+ }
+ else
+ {
+ isNoted = false;
+ }
+ }
+ if (!BannedUtxos.TryAdd(utxo, (severity, timeOfBan, isNoted, bannedForRound)))
+ {
+ var elem = BannedUtxos[utxo];
+ if (elem.severity != severity)
+ {
+ elem.severity = severity;
+ updatedOrAdded = true;
+ }
+ if (elem.isNoted != isNoted)
+ {
+ elem.isNoted = isNoted;
+ updatedOrAdded = true;
+ }
+ if (elem.bannedForRound != bannedForRound)
+ {
+ elem.bannedForRound = bannedForRound;
+ updatedOrAdded = true;
+ }
+ }
+ else
+ {
+ append = true;
+ updatedOrAdded = true;
+
+ string line = $"{timeOfBan.ToString(CultureInfo.InvariantCulture)}:{severity}:{utxo.N}:{utxo.Hash}:{isNoted}:{bannedForRound}";
lines.Add(line);
- Logger.LogInfo<UtxoReferee>($"UTXO banned with severity: {severity}. UTXO: {utxo.N}:{utxo.Hash}.");
}
+
+ Logger.LogInfo<UtxoReferee>($"UTXO {(isNoted ? "noted" : "banned")} with severity: {severity}. UTXO: {utxo.N}:{utxo.Hash}.");
}
+ if (updatedOrAdded)
+ {
+ if (append)
+ {
if (lines.Count != 0)
{
await File.AppendAllLinesAsync(BannedUtxosFilePath, lines);
}
}
+ else
+ {
+ var allLines = BannedUtxos.Select(x => $"{x.Value.timeOfBan.ToString(CultureInfo.InvariantCulture)}:{x.Value.severity}:{x.Key.N}:{x.Key.Hash}:{x.Value.isNoted}:{x.Value.bannedForRound}");
+ await File.WriteAllLinesAsync(BannedUtxosFilePath, allLines);
+ }
+ }
+ }
public async Task UnbanAsync(OutPoint output)
{
if (BannedUtxos.TryRemove(output, out _))
{
- IEnumerable<string> lines = BannedUtxos.Select(x => $"{x.Value.timeOfBan.ToString(CultureInfo.InvariantCulture)}:{x.Value.severity}:{x.Key.N}:{x.Key.Hash}");
+ IEnumerable<string> lines = BannedUtxos.Select(x => $"{x.Value.timeOfBan.ToString(CultureInfo.InvariantCulture)}:{x.Value.severity}:{x.Key.N}:{x.Key.Hash}:{x.Value.isNoted}:{x.Value.bannedForRound}");
await File.WriteAllLinesAsync(BannedUtxosFilePath, lines);
Logger.LogInfo<UtxoReferee>($"UTXO unbanned: {output.N}:{output.Hash}.");
}
}
+
+ public async Task<(int severity, TimeSpan bannedRemaining, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound)?> TryGetBannedAsync(OutPoint outpoint, bool notedToo)
+ {
+ if (BannedUtxos.TryGetValue(outpoint, out (int severity, DateTimeOffset timeOfBan, bool isNoted, long bannedForRound) bannedElem))
+ {
+ int maxBan = (int)TimeSpan.FromHours(RoundConfig.DosDurationHours.Value).TotalMinutes;
+ var bannedRemaining = DateTimeOffset.UtcNow - bannedElem.timeOfBan;
+ int banLeftMinutes = maxBan - (int)bannedRemaining.TotalMinutes;
+ if (banLeftMinutes > 0)
+ {
+ if (bannedElem.isNoted)
+ {
+ if (notedToo)
+ {
+ return (bannedElem.severity, bannedRemaining, bannedElem.timeOfBan, true, bannedElem.bannedForRound);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ return (bannedElem.severity, bannedRemaining, bannedElem.timeOfBan, false, bannedElem.bannedForRound);
+ }
+ }
+ else
+ {
+ await UnbanAsync(outpoint);
+ }
+ }
+ return null;
+ }
+
+ public int CountBanned(bool notedToo)
+ {
+ if (notedToo)
+ {
+ return BannedUtxos.Count;
+ }
+ else
+ {
+ return BannedUtxos.Count(x => !x.Value.isNoted);
+ }
+ }
}
}

MODIFY
WalletWasabi/WebClients/Wasabi/ChaumianCoinJoin/AliceClient.cs
WalletWasabi/WebClients/Wasabi/ChaumianCoinJoin/AliceClient.cs
@@ -15,6 +15,7 @@ using WalletWasabi.TorSocks5;
using WalletWasabi.Bases;
using System.Threading;
using WalletWasabi.Exceptions;
+using System.Diagnostics;
namespace WalletWasabi.WebClients.Wasabi.ChaumianCoinJoin
{

Try implementing noting