Add round independent CoinVerifier
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -23,6 +23,7 @@ using WalletWasabi.CoinJoin.Coordinator.Rounds;
using WalletWasabi.Extensions;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
+using WalletWasabi.WabiSabi.Backend.Banning;
using static WalletWasabi.Crypto.SchnorrBlinding;
namespace WalletWasabi.Backend.Controllers;
@@ -45,6 +46,7 @@ public class ChaumianCoinJoinController : ControllerBase
private IRPCClient RpcClient => Global.RpcClient;
private Network Network => Global.Config.Network;
private Coordinator Coordinator => Global.Coordinator;
+ private CoinVerifier? CoinVerifier => Global.CoinVerifier;
private static AsyncLock InputsLock { get; } = new AsyncLock();
private static AsyncLock OutputLock { get; } = new AsyncLock();
@@ -203,6 +205,7 @@ public class ChaumianCoinJoinController : ControllerBase
uint256 blindedOutputScriptsHash = new(Hashes.SHA256(blindedOutputScriptHashesByte));
var inputs = new HashSet<Coin>();
+ var coinAndTxOutResponses = new Dictionary<Coin, GetTxOutResponse>();
var allInputsConfirmed = true;
foreach (var responses in getTxOutResponses)
@@ -243,7 +246,9 @@ public class ChaumianCoinJoinController : ControllerBase
return BadRequest("Provided proof is invalid.");
}
+ var coin = new Coin(inputProof.Input, txOut);
inputs.Add(new Coin(inputProof.Input, txOut));
+ coinAndTxOutResponses.Add(coin, getTxOutResponse);
}
if (!allInputsConfirmed)
@@ -301,6 +306,13 @@ public class ChaumianCoinJoinController : ControllerBase
{
round.RemoveAlicesBy(aliceToRemove);
}
+
+ foreach (var coin in alice.Inputs)
+ {
+ var delayUntilVerificationStart = round.InputRegistrationTimesout - DateTimeOffset.UtcNow - CoinVerifier?.WabiSabiConfig.CoinVerifierStartBefore ?? TimeSpan.FromMinutes(2);
+
+ CoinVerifier?.ScheduleVerification(coin, CancellationToken.None, delayUntilVerificationStart, false, coinAndTxOutResponses[coin].Confirmations);
+ }
round.AddAlice(alice);
// All checks are good. Sign.

MODIFY
WalletWasabi.Backend/Global.cs
WalletWasabi.Backend/Global.cs
@@ -66,6 +66,7 @@ public class Global : IDisposable
private HttpClient HttpClient { get; }
public Coordinator? Coordinator { get; private set; }
+ public CoinVerifier? CoinVerifier { get; private set; }
public Config Config { get; }
@@ -114,6 +115,7 @@ public class Global : IDisposable
var coinVerifierApiClient = new CoinVerifierApiClient(CoordinatorParameters.RuntimeCoordinatorConfig.CoinVerifierApiAuthToken, RpcClient.Network, HttpClient);
var whitelist = await Whitelist.CreateAndLoadFromFileAsync(CoordinatorParameters.WhitelistFilePath, cancel).ConfigureAwait(false);
coinVerifier = new(CoinJoinIdStore, coinVerifierApiClient, whitelist, CoordinatorParameters.RuntimeCoordinatorConfig);
+ CoinVerifier = coinVerifier;
Logger.LogInfo("CoinVerifier created successfully.");
}
catch (Exception exc)

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/CoinVerifierTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/CoinVerifierTests.cs
@@ -41,7 +41,8 @@ public class CoinVerifierTests
List<Coin> generatedCoins = GenerateCoins(98);
List<Coin> naughtyCoins = new();
- await foreach (var item in coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
+ ScheduleVerifications(coinVerifier, generatedCoins);
+ foreach (var item in await coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
{
if (item.ShouldBan)
{
@@ -55,21 +56,21 @@ public class CoinVerifierTests
[Fact]
public async Task CanFilterNaughtyUtxoTestAsync()
{
+ using HttpResponseMessage dirtyResponse = new(System.Net.HttpStatusCode.OK) { Content = new StringContent(GenerateDirtyJsonReport()) };
+ using HttpResponseMessage cleanResponse = new(System.Net.HttpStatusCode.OK) { Content = new StringContent(GenerateCleanJsonReport()) };
+
Mock<HttpClient> mockHttpClient = new();
mockHttpClient.SetupSequence(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
- .ReturnsAsync(() =>
- {
- string content = GenerateDirtyJsonReport();
- HttpResponseMessage response = new(System.Net.HttpStatusCode.OK);
- response.Content = new StringContent(content);
- return response;
- }).ReturnsAsync(() =>
- {
- string content = GenerateCleanJsonReport();
- HttpResponseMessage response = new(System.Net.HttpStatusCode.OK);
- response.Content = new StringContent(content);
- return response;
- });
+ .ReturnsAsync(dirtyResponse)
+ .ReturnsAsync(cleanResponse)
+ .ReturnsAsync(cleanResponse)
+ .ThrowsAsync(new InvalidOperationException())
+ .ReturnsAsync(cleanResponse)
+ .ReturnsAsync(cleanResponse)
+ .ReturnsAsync(cleanResponse)
+ .ReturnsAsync(cleanResponse)
+ .ReturnsAsync(cleanResponse)
+ .ReturnsAsync(cleanResponse);
mockHttpClient.Object.BaseAddress = new Uri(TestURL);
@@ -79,15 +80,25 @@ public class CoinVerifierTests
CoinVerifier coinVerifier = new(coinJoinIdStore, apiClient, _wabisabiTestConfig);
List<Coin> generatedCoins = GenerateCoins(10);
+ List<Coin> removedCoins = new();
+ List<Coin> checkedCoins = new();
- await foreach (var item in coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
+ ScheduleVerifications(coinVerifier, generatedCoins);
+ foreach (var item in await coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
{
+ checkedCoins.Add(item.Coin);
if (item.ShouldBan)
{
naughtyCoins.Add(item.Coin);
}
+ else if (item.ShouldRemove)
+ {
+ removedCoins.Add(item.Coin);
+ }
}
+ Assert.Equal(10, checkedCoins.Count);
+ Assert.Single(removedCoins);
Assert.Single(naughtyCoins);
}
@@ -112,7 +123,8 @@ public class CoinVerifierTests
List<Coin> generatedCoins = GenerateCoins(5);
- await foreach (var item in coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
+ ScheduleVerifications(coinVerifier, generatedCoins);
+ foreach (var item in await coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
{
if (item.ShouldBan)
{
@@ -147,7 +159,8 @@ public class CoinVerifierTests
CoinVerifierApiClient apiClient = new("token", Network.Main, mockHttpClient.Object);
CoinVerifier coinVerifier = new(coinJoinIdStore, apiClient, _wabisabiTestConfig);
- await foreach (var item in coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
+ ScheduleVerifications(coinVerifier, generatedCoins);
+ foreach (var item in await coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
{
Assert.False(item.ShouldBan);
}
@@ -175,7 +188,8 @@ public class CoinVerifierTests
List<Coin> generatedCoins = GenerateCoins(10);
- await foreach (var item in coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
+ ScheduleVerifications(coinVerifier, generatedCoins);
+ foreach (var item in await coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
{
if (item.ShouldBan)
{
@@ -208,4 +222,12 @@ public class CoinVerifierTests
return coins;
}
+
+ private void ScheduleVerifications(CoinVerifier coinVerifier, IEnumerable<Coin> coins)
+ {
+ foreach (Coin coin in coins)
+ {
+ coinVerifier.ScheduleVerification(coin, CancellationToken.None);
+ }
+ }
}

MODIFY
WalletWasabi/CoinJoin/Coordinator/Coordinator.cs
WalletWasabi/CoinJoin/Coordinator/Coordinator.cs
@@ -127,6 +127,11 @@ public class Coordinator : IDisposable
}
BlockNotifier.OnBlock += BlockNotifier_OnBlockAsync;
+
+ if (CoinVerifier is { })
+ {
+ CoinVerifier.CoinBlackListed += CoinVerifier_CoinBlackListedAsync;
+ }
}
public event EventHandler<Transaction>? CoinJoinBroadcasted;
@@ -433,6 +438,18 @@ public class Coordinator : IDisposable
}
}
+ private async void CoinVerifier_CoinBlackListedAsync(object? _, Coin coin)
+ {
+ foreach (var round in Rounds)
+ {
+ if (round.ContainsInput(coin.Outpoint, out var alices))
+ {
+ var outPointsToBan = alices.SelectMany(a => a.Inputs).Select(i => i.Outpoint).ToArray();
+ await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted: false, round.RoundId, forceBan: true, outPointsToBan).ConfigureAwait(false);
+ }
+ }
+ }
+
#region IDisposable Support
protected virtual void Dispose(bool disposing)
@@ -446,6 +463,11 @@ public class Coordinator : IDisposable
BlockNotifier.OnBlock -= BlockNotifier_OnBlockAsync;
}
+ if (CoinVerifier is { })
+ {
+ CoinVerifier.CoinBlackListed -= CoinVerifier_CoinBlackListedAsync;
+ }
+
foreach (CoordinatorRound round in Rounds)
{
round.StatusChanged -= Round_StatusChangedAsync;

MODIFY
WalletWasabi/CoinJoin/Coordinator/Rounds/CoordinatorRound.cs
WalletWasabi/CoinJoin/Coordinator/Rounds/CoordinatorRound.cs
@@ -643,15 +643,24 @@ public class CoordinatorRound
{
return;
}
- List<OutPoint> inputsToBan = new();
+
try
{
- var coinsToCheck = Alices.SelectMany(a => a.Inputs);
- await foreach (var info in CoinVerifier.VerifyCoinsAsync(coinsToCheck, CancellationToken.None, RoundId.ToString()))
+ Dictionary<Coin, Alice> coinDictionary = new();
+ foreach (var alice in Alices)
+ {
+ foreach (var coin in alice.Inputs)
+ {
+ coinDictionary.Add(coin, alice);
+ }
+ }
+
+ foreach (var info in await CoinVerifier.VerifyCoinsAsync(coinDictionary.Keys, CancellationToken.None).ConfigureAwait(false))
{
- if (info.ShouldBan)
+ if (info.ShouldRemove)
{
- inputsToBan.Add(info.Coin.Outpoint);
+ var aliceToRemove = coinDictionary[info.Coin];
+ Alices.Remove(aliceToRemove);
}
}
}
@@ -659,14 +668,6 @@ public class CoordinatorRound
{
Logger.LogError($"{nameof(CoinVerifier)} has failed to verify all Alices({Alices.Count}).", exc);
}
-
- var alicesToRemove = Alices.Where(alice => inputsToBan.Any(outpoint => alice.Inputs.Select(input => input.Outpoint).Contains(outpoint))).ToArray();
- Logger.LogInfo($"Alices({alicesToRemove.Length}) was force banned in round '{RoundId}'.");
- foreach (var alice in alicesToRemove)
- {
- Alices.Remove(alice);
- }
- await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted: false, RoundId, forceBan: true, inputsToBan.ToArray()).ConfigureAwait(false);
}
private async Task MoveToInputRegistrationAsync()

MODIFY
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifier.cs
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifier.cs
using NBitcoin;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
-using System.Runtime.CompilerServices;
using System.Threading;
+using System.Threading.Tasks;
using WalletWasabi.Logging;
using WalletWasabi.WabiSabi.Backend.Rounds.CoinJoinStorage;
-using WalletWasabi.WabiSabi.Backend.Statistics;
namespace WalletWasabi.WabiSabi.Backend.Banning;
-public record CoinVerifyInfo(bool ShouldBan, Coin Coin);
-
public class CoinVerifier
{
public CoinVerifier(CoinJoinIdStore coinJoinIdStore, CoinVerifierApiClient apiClient, Whitelist whitelist, WabiSabiConfig wabiSabiConfig)
@@ -34,74 +32,55 @@ public class CoinVerifier
public WabiSabiConfig WabiSabiConfig { get; }
private CoinJoinIdStore CoinJoinIdStore { get; }
private CoinVerifierApiClient CoinVerifierApiClient { get; }
+ private ConcurrentDictionary<Coin, TaskCompletionSource<CoinVerifyResult>> CoinVerifyItems { get; } = new();
- private bool CheckIfAlreadyVerified(Coin coin)
- {
- // Step 1: Check if address is whitelisted.
- if (Whitelist.TryGet(coin.Outpoint, out _))
- {
- return true;
- }
+ public event EventHandler<Coin>? CoinBlackListed;
- // Step 2: Check if address is from a coinjoin.
- if (CoinJoinIdStore.Contains(coin.Outpoint.Hash))
+ public async Task<IEnumerable<CoinVerifyResult>> VerifyCoinsAsync(IEnumerable<Coin> coinsToCheck, CancellationToken cancellationToken)
{
- return true;
- }
-
- return false;
- }
-
- public async IAsyncEnumerable<CoinVerifyInfo> VerifyCoinsAsync(IEnumerable<Coin> coinsToCheck, [EnumeratorCancellation] CancellationToken cancellationToken, string roundId = "")
- {
- var before = DateTimeOffset.UtcNow;
-
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(30));
using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, cancellationToken);
- var lastChangeId = Whitelist.ChangeId;
- Whitelist.RemoveAllExpired(WabiSabiConfig.ReleaseFromWhitelistAfter);
-
- var scriptsToCheck = new HashSet<Script>();
- var innocentsCounter = 0;
+ // Booting up the results with the default value - ban: no, remove: yes.
+ Dictionary<Coin, CoinVerifyResult> coinVerifyItems = coinsToCheck.ToDictionary(c => c, c => new CoinVerifyResult(c, false, true));
+ List<Task<CoinVerifyResult>> tasks = new();
foreach (var coin in coinsToCheck)
{
- if (CheckIfAlreadyVerified(coin))
+ if (CoinVerifyItems.TryGetValue(coin, out var taskCompletionSource))
{
- innocentsCounter++;
- yield return new CoinVerifyInfo(false, coin);
- }
- else
- {
- scriptsToCheck.Add(coin.ScriptPubKey);
+ tasks.Add(taskCompletionSource.Task);
}
}
- Logger.LogInfo($"{innocentsCounter} out of {coinsToCheck.Count()} utxo is already verified in Round({roundId}).");
- await foreach (var response in CoinVerifierApiClient.VerifyScriptsAsync(scriptsToCheck, linkedCts.Token))
+ try
+ {
+ do
{
- bool shouldBanUtxo = CheckForFlags(response.ApiResponseItem);
+ var completedTask = await Task.WhenAny(tasks).WaitAsync(linkedCts.Token).ConfigureAwait(false);
+ tasks.Remove(completedTask);
+ var result = await completedTask.WaitAsync(linkedCts.Token).ConfigureAwait(false);
- // Find all coins with the same script (address reuse).
- foreach (var coin in coinsToCheck.Where(c => c.ScriptPubKey == response.ScriptPubKey))
+ // Update the default value with the real result.
+ coinVerifyItems[result.Coin] = result;
+ }
+ while (tasks.Any());
+ }
+ catch (OperationCanceledException ex)
{
- if (!shouldBanUtxo)
+ if (cancellationTokenSource.IsCancellationRequested)
{
- Whitelist.Add(coin.Outpoint);
+ Logger.LogError(ex);
}
- yield return new CoinVerifyInfo(shouldBanUtxo, coin);
- }
+ // Otherwise just return.
}
-
- if (Whitelist.ChangeId != lastChangeId)
+ catch (Exception ex)
{
- Whitelist.WriteToFile();
+ Logger.LogError(ex);
}
- var duration = DateTimeOffset.UtcNow - before;
- RequestTimeStatista.Instance.Add("verifier-period", duration);
+ return coinVerifyItems.Values.ToArray();
}
private bool CheckForFlags(ApiResponseItem response)
@@ -125,4 +104,71 @@ public class CoinVerifier
return shouldBan;
}
+
+ public void ScheduleVerification(Coin coin, CancellationToken cancellationToken, TimeSpan? delayedStart = null, bool oneHop = false, int? confirmations = null)
+ {
+ TaskCompletionSource<CoinVerifyResult> taskCompletionSource = new();
+ CoinVerifyItems.TryAdd(coin, taskCompletionSource);
+
+ if (oneHop)
+ {
+ taskCompletionSource.SetResult(new CoinVerifyResult(coin, false, false));
+ return;
+ }
+
+ if (Whitelist.TryGet(coin.Outpoint, out _))
+ {
+ taskCompletionSource.SetResult(new CoinVerifyResult(coin, false, false));
+ return;
+ }
+
+ if (CoinJoinIdStore.Contains(coin.Outpoint.Hash))
+ {
+ taskCompletionSource.SetResult(new CoinVerifyResult(coin, false, false));
+ return;
+ }
+
+ if (confirmations is { } confs && coin.Amount > WabiSabiConfig.CoinVerifierRequiredConfirmationAmount && confs < WabiSabiConfig.CoinVerifierRequiredConfirmation)
+ {
+ taskCompletionSource.SetResult(new CoinVerifyResult(coin, false, ShouldRemove: true));
+ return;
+ }
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ delayedStart ??= TimeSpan.Zero;
+
+ if (delayedStart > TimeSpan.Zero)
+ {
+ await Task.Delay(delayedStart.Value, cancellationToken).ConfigureAwait(false);
+ }
+
+ // Define a timeout.
+ using CancellationTokenSource timeout = new(TimeSpan.FromMinutes(2));
+ using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeout.Token);
+ var apiResponseItem = await CoinVerifierApiClient.SendRequestAsync(coin.ScriptPubKey, linkedCts.Token).ConfigureAwait(false);
+ var shouldBan = CheckForFlags(apiResponseItem);
+
+ // We got a definetive answer.
+ if (shouldBan)
+ {
+ CoinBlackListed?.Invoke(this, coin);
+ }
+ else
+ {
+ Whitelist.Add(coin.Outpoint);
+ }
+
+ taskCompletionSource.SetResult(new CoinVerifyResult(coin, shouldBan, ShouldRemove: shouldBan));
+ }
+ catch (Exception)
+ {
+ taskCompletionSource.SetResult(new CoinVerifyResult(coin, false, ShouldRemove: true));
+
+ // Do not throw an exception here - unobserverved exception prevention.
+ }
+ }, cancellationToken);
+ }
}

MODIFY
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifierApiClient.cs
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifierApiClient.cs
@@ -29,7 +29,7 @@ public class CoinVerifierApiClient
private HttpClient HttpClient { get; set; }
- public virtual async Task<(ApiResponseItem ApiResponseItem, Script Script)> SendRequestAsync(Script script, CancellationToken cancellationToken)
+ public virtual async Task<ApiResponseItem> SendRequestAsync(Script script, CancellationToken cancellationToken)
{
if (HttpClient.BaseAddress is null)
{
@@ -62,39 +62,6 @@ public class CoinVerifierApiClient
ApiResponseItem deserializedRecord = JsonConvert.DeserializeObject<ApiResponseItem>(responseString)
?? throw new JsonSerializationException($"Failed to deserialize API response, response string was: '{responseString}'");
- return (deserializedRecord, script);
- }
-
- public async IAsyncEnumerable<(ApiResponseItem ApiResponseItem, Script ScriptPubKey)> VerifyScriptsAsync(IEnumerable<Script> scripts, [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- IEnumerable<IEnumerable<Script>> chunks = scripts.Chunk(100);
-
- foreach (var chunk in chunks)
- {
- var tasks = chunk.Select(script => SendRequestAsync(script, cancellationToken)).ToList();
-
- foreach (var task in tasks)
- {
- (ApiResponseItem ApiResponseItem, Script ScriptPubKey) response;
- try
- {
- var completedTask = await Task.WhenAny(task).ConfigureAwait(false);
-
- response = await completedTask.ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- Logger.LogWarning($"API response didn't arrive in time, operation was cancelled.");
- continue;
- }
- catch (Exception ex)
- {
- Logger.LogError(ex);
- continue;
- }
-
- yield return response;
- }
- }
+ return deserializedRecord;
}
}

ADD
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifyItem.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.WabiSabi.Backend.Banning;
+
+public class CoinVerifyWorkerItem
+{
+ public CoinVerifyWorkerItem(Coin coin, TaskCompletionSource<CoinVerifyResult> taskCompletionSourceToSet)
+ {
+ Coin = coin;
+ TaskCompletionSourceToSet = taskCompletionSourceToSet;
+ }
+
+ public Coin Coin { get; }
+ public TaskCompletionSource<CoinVerifyResult> TaskCompletionSourceToSet { get; }
+ private TaskCompletionSource<CoinVerifyResult> Result { get; } = new();
+
+ public void Start(CancellationToken token)
+ {
+ }
+}

ADD
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifyResult.cs
+using NBitcoin;
+
+namespace WalletWasabi.WabiSabi.Backend.Banning;
+
+public record CoinVerifyResult(Coin Coin, bool ShouldBan, bool ShouldRemove);

MODIFY
WalletWasabi/WabiSabi/Backend/DoSPrevention/Prison.cs
WalletWasabi/WabiSabi/Backend/DoSPrevention/Prison.cs
@@ -42,8 +42,11 @@ public class Prison
}
public void Ban(Alice alice, uint256 lastDisruptedRoundId, bool isLongBan = false)
+ => Ban(alice.Coin.Outpoint, lastDisruptedRoundId, isLongBan);
+
+ public void Ban(OutPoint utxo, uint256 lastDisruptedRoundId, bool isLongBan = false)
{
- Punish(alice.Coin.Outpoint, isLongBan ? Punishment.LongBanned : Punishment.Banned, lastDisruptedRoundId);
+ Punish(utxo, isLongBan ? Punishment.LongBanned : Punishment.Banned, lastDisruptedRoundId);
}
public void Ban(OutPoint outpoint, uint256 lastDisruptedRoundId)

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
@@ -32,7 +32,7 @@ public partial class Arena : IWabiSabiApiRequestHandler
private async Task<InputRegistrationResponse> RegisterInputCoreAsync(InputRegistrationRequest request, CancellationToken cancellationToken)
{
- var coin = await OutpointToCoinAsync(request, cancellationToken).ConfigureAwait(false);
+ var (coin, confirmations) = await OutpointToCoinAsync(request, cancellationToken).ConfigureAwait(false);
using (await AsyncLock.LockAsync(cancellationToken).ConfigureAwait(false))
{
@@ -67,19 +67,21 @@ public partial class Arena : IWabiSabiApiRequestHandler
// only that the probability of duplicates is very low).
var id = new Guid(SecureRandom.Instance.GetBytes(16));
- var isPayingZeroCoordinationFee = CoinJoinIdStore.Contains(coin.Outpoint.Hash);
+ var comingFromCoinJoin = CoinJoinIdStore.Contains(coin.Outpoint.Hash);
+ bool oneHop = false;
- if (!isPayingZeroCoordinationFee)
+ if (!comingFromCoinJoin)
{
// If the coin comes from a tx that all of the tx inputs are coming from a CJ (1 hop - no pay).
Transaction tx = await Rpc.GetRawTransactionAsync(coin.Outpoint.Hash, true, cancellationToken).ConfigureAwait(false);
if (tx.Inputs.All(input => CoinJoinIdStore.Contains(input.PrevOut.Hash)))
{
- isPayingZeroCoordinationFee = true;
+ oneHop = true;
}
}
+ var isPayingZeroCoordinationFee = comingFromCoinJoin && oneHop;
var alice = new Alice(coin, request.OwnershipProof, round, id, isPayingZeroCoordinationFee);
if (alice.CalculateRemainingAmountCredentials(round.Parameters.MiningFeeRate, round.Parameters.CoordinationFeeRate) <= Money.Zero)
@@ -115,6 +117,9 @@ public partial class Arena : IWabiSabiApiRequestHandler
alice.SetDeadlineRelativeTo(round.ConnectionConfirmationTimeFrame.Duration);
round.Alices.Add(alice);
+ var delayUntilVerificationStart = round.InputRegistrationTimeFrame.EndTime - DateTimeOffset.UtcNow - Config.CoinVerifierStartBefore;
+ CoinVerifier?.ScheduleVerification(coin, cancellationToken, delayUntilVerificationStart, oneHop, confirmations);
+
return new(alice.Id,
commitAmountCredentialResponse,
commitVsizeCredentialResponse,
@@ -355,7 +360,7 @@ public partial class Arena : IWabiSabiApiRequestHandler
await zeroVsizeTask.ConfigureAwait(false));
}
- public async Task<Coin> OutpointToCoinAsync(InputRegistrationRequest request, CancellationToken cancellationToken)
+ public async Task<(Coin coin, int Confirmations)> OutpointToCoinAsync(InputRegistrationRequest request, CancellationToken cancellationToken)
{
OutPoint input = request.Input;
@@ -391,7 +396,7 @@ public partial class Arena : IWabiSabiApiRequestHandler
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputImmature);
}
- return new Coin(input, txOutResponse.TxOut);
+ return (new Coin(input, txOutResponse.TxOut), txOutResponse.Confirmations);
}
public Task<RoundStateResponse> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -44,6 +44,11 @@ public partial class Arena : PeriodicRunner
RoundParameterFactory = roundParameterFactory;
CoinVerifier = coinVerifier;
MaxSuggestedAmountProvider = new(Config);
+
+ if (CoinVerifier is not null)
+ {
+ CoinVerifier.CoinBlackListed += CoinVerifier_CoinBlackListed;
+ }
}
public event EventHandler<Transaction>? CoinJoinBroadcast;
@@ -123,20 +128,14 @@ public partial class Arena : PeriodicRunner
{
try
{
- int banCounter = 0;
- var aliceDictionary = round.Alices.Where(x => !x.IsPayingZeroCoordinationFee).ToDictionary(a => a.Coin, a => a);
- IEnumerable<Coin> coinsToCheck = aliceDictionary.Values.Select(x => x.Coin);
- await foreach (var coinVerifyInfo in CoinVerifier.VerifyCoinsAsync(coinsToCheck, cancel, round.Id.ToString()).ConfigureAwait(false))
+ var aliceDictionary = round.Alices.ToDictionary(a => a.Coin, a => a);
+ foreach (var coinVerifyInfo in await CoinVerifier.VerifyCoinsAsync(aliceDictionary.Keys, cancel).ConfigureAwait(false))
{
- if (coinVerifyInfo.ShouldBan)
+ if (coinVerifyInfo.ShouldRemove)
{
- banCounter++;
- Alice aliceToPunish = aliceDictionary[coinVerifyInfo.Coin];
- Prison.Ban(aliceToPunish, round.Id, isLongBan: true);
- round.Alices.Remove(aliceToPunish);
+ round.Alices.Remove(aliceDictionary[coinVerifyInfo.Coin]);
}
}
- Logger.LogInfo($"{banCounter} utxos were banned from round {round.Id}.");
}
catch (Exception exc)
{
@@ -632,4 +631,21 @@ public partial class Arena : PeriodicRunner
return coordinatorScriptPubKey;
}
+
+ private void CoinVerifier_CoinBlackListed(object? _, Coin coin)
+ {
+ // For logging reason Prison needs the roundId.
+ var roundState = RoundStates.FirstOrDefault(rs => rs.CoinjoinState.Inputs.Any(input => input.Outpoint == coin.Outpoint));
+ var roundId = roundState?.Id ?? uint256.Zero;
+ Prison.Ban(coin.Outpoint, roundId, isLongBan: true);
+ }
+
+ public override void Dispose()
+ {
+ if (CoinVerifier is not null)
+ {
+ CoinVerifier.CoinBlackListed -= CoinVerifier_CoinBlackListed;
+ }
+ base.Dispose();
+ }
}

MODIFY
WalletWasabi/WabiSabi/Backend/WabiSabiConfig.cs
WalletWasabi/WabiSabi/Backend/WabiSabiConfig.cs
@@ -128,6 +128,19 @@ public class WabiSabiConfig : ConfigBase
[JsonProperty(PropertyName = "CoinVerifierApiAuthToken", DefaultValueHandling = DefaultValueHandling.Populate)]
public string CoinVerifierApiAuthToken { get; set; } = "";
+ [DefaultValueTimeSpan("0d 0h 2m 0s")]
+ [JsonProperty(PropertyName = "CoinVerifierStartBefore", DefaultValueHandling = DefaultValueHandling.Populate)]
+ public TimeSpan CoinVerifierStartBefore { get; set; } = TimeSpan.FromMinutes(2);
+
+ [DefaultValue(6)]
+ [JsonProperty(PropertyName = "CoinVerifierRequiredConfirmation", DefaultValueHandling = DefaultValueHandling.Populate)]
+ public int CoinVerifierRequiredConfirmation { get; set; } = 6;
+
+ [DefaultValueMoneyBtc("1")]
+ [JsonProperty(PropertyName = "CoinVerifierRequiredConfirmationAmount", DefaultValueHandling = DefaultValueHandling.Populate)]
+ [JsonConverter(typeof(MoneyBtcJsonConverter))]
+ public Money CoinVerifierRequiredConfirmationAmount { get; set; } = Money.Coins(1m);
+
[DefaultValueTimeSpan("31d 0h 0m 0s")]
[JsonProperty(PropertyName = "ReleaseFromWhitelistAfter", DefaultValueHandling = DefaultValueHandling.Populate)]
public TimeSpan ReleaseFromWhitelistAfter { get; set; } = TimeSpan.FromDays(31);

Add round independent CoinVerifier