Finish implementing input banning
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -243,10 +243,19 @@ namespace WalletWasabi.Backend.Controllers
}
// Progress round if needed.
+ if(round.CountAlices() >= round.AnonymitySet)
+ {
+ var alicesToBan = await round.RemoveAlicesIfInputsSpentAsync();
+ if (alicesToBan.Count() != 0)
+ {
+ await Coordinator.UtxoReferee.BanUtxosAsync(1, DateTimeOffset.Now, alicesToBan.SelectMany(x => x.Inputs).Select(y => y.OutPoint).ToArray());
+ }
+
if (round.CountAlices() >= round.AnonymitySet)
{
await round.ExecuteNextPhaseAsync(CcjRoundPhase.ConnectionConfirmation);
}
+ }
var resp = new InputsResponse
{

MODIFY
WalletWasabi.Backend/Global.cs
WalletWasabi.Backend/Global.cs
@@ -55,9 +55,10 @@ namespace WalletWasabi.Backend
var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{RpcClient.Network}.dat");
var utxoSetFilePath = Path.Combine(indexBuilderServiceDir, $"UtxoSet{RpcClient.Network}.dat");
IndexBuilderService = new IndexBuilderService(RpcClient, indexFilePath, utxoSetFilePath);
+ Coordinator = new CcjCoordinator(RpcClient.Network, Path.Combine(DataDir, nameof(CcjCoordinator)), RpcClient, roundConfig);
+ IndexBuilderService.NewBlock += IndexBuilderService_NewBlockAsync;
IndexBuilderService.Synchronize();
- Coordinator = new CcjCoordinator(RpcClient.Network, Path.Combine(DataDir,nameof(CcjCoordinator)), RpcClient, roundConfig);
await Coordinator.MakeSureTwoRunningRoundsAsync();
if (roundConfig.FilePath != null)
@@ -80,6 +81,11 @@ namespace WalletWasabi.Backend
}
}
+ public static async void IndexBuilderService_NewBlockAsync(object sender, Block block)
+ {
+ await Coordinator.ProcessBlockAsync(block);
+ }
+
private static async Task AssertRpcNodeFullyInitializedAsync()
{
try

MODIFY
WalletWasabi.Backend/Startup.cs
WalletWasabi.Backend/Startup.cs
@@ -73,6 +73,8 @@ namespace WalletWasabi.Backend
if(Global.IndexBuilderService != null)
{
+ Global.IndexBuilderService.NewBlock -= Global.IndexBuilderService_NewBlockAsync;
+
var t = Global.IndexBuilderService.StopAsync();
stopTasks.Add(t);
}

MODIFY
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
+using WalletWasabi.Services;
namespace WalletWasabi.ChaumianCoinJoin
{
@@ -102,11 +103,14 @@ namespace WalletWasabi.ChaumianCoinJoin
public TimeSpan SigningTimeout { get; }
- public CcjRound(RPCClient rpc, CcjRoundConfig config)
+ public UtxoReferee UtxoReferee { get; }
+
+ public CcjRound(RPCClient rpc, UtxoReferee utxoReferee, CcjRoundConfig config)
{
Interlocked.Increment(ref RoundCount);
RpcClient = Guard.NotNull(nameof(rpc), rpc);
+ UtxoReferee = Guard.NotNull(nameof(utxoReferee), utxoReferee);
Guard.NotNull(nameof(config), config);
Denomination = config.Denomination;
@@ -319,6 +323,11 @@ namespace WalletWasabi.ChaumianCoinJoin
{
// Only fail if less two one Alice is registered.
// Don't ban anyone, it's ok if they lost connection.
+ var alicesToBan = await RemoveAlicesIfInputsSpentAsync();
+ if (alicesToBan.Count() != 0)
+ {
+ await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.Now, alicesToBan.SelectMany(x => x.Inputs).Select(y => y.OutPoint).ToArray());
+ }
int aliceCountAfterInputRegistrationTimeout = CountAlices();
if (aliceCountAfterInputRegistrationTimeout < 2)
{
@@ -334,9 +343,14 @@ namespace WalletWasabi.ChaumianCoinJoin
break;
case CcjRoundPhase.ConnectionConfirmation:
{
- // Only fail if less two one Alice is registered.
+ // Only fail if less than two one alices are registered.
// Don't ban anyone, it's ok if they lost connection.
RemoveAlicesBy(AliceState.InputsRegistered);
+ var alicesToBan = await RemoveAlicesIfInputsSpentAsync();
+ if (alicesToBan.Count() != 0)
+ {
+ await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.Now, alicesToBan.SelectMany(x => x.Inputs).Select(y => y.OutPoint).ToArray());
+ }
int aliceCountAfterConnectionConfirmationTimeout = CountAlices();
if (aliceCountAfterConnectionConfirmationTimeout < 2)
{
@@ -360,6 +374,11 @@ namespace WalletWasabi.ChaumianCoinJoin
break;
case CcjRoundPhase.Signing:
{
+ var alicesToBan = await RemoveAlicesIfInputsSpentAsync();
+ if (alicesToBan.Count() != 0)
+ {
+ await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.Now, alicesToBan.SelectMany(x => x.Inputs).Select(y => y.OutPoint).ToArray());
+ }
Fail();
}
break;
@@ -555,6 +574,34 @@ namespace WalletWasabi.ChaumianCoinJoin
}
}
+ public async Task<IEnumerable<Alice>> RemoveAlicesIfInputsSpentAsync()
+ {
+ using (RoundSyncronizerLock.Lock())
+ {
+ if (Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation || Status != CcjRoundStatus.Running)
+ {
+ throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
+ }
+
+ var alicesRemoved = new List<Alice>();
+ foreach (Alice alice in Alices)
+ {
+ foreach (OutPoint input in alice.Inputs.Select(y => y.OutPoint))
+ {
+ GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(input.Hash, (int)input.N, includeMempool: true);
+
+ // Check if inputs are unspent.
+ if (getTxOutResponse == null)
+ {
+ alicesRemoved.Add(alice);
+ Alices.Remove(alice);
+ }
+ }
+ }
+ return alicesRemoved;
+ }
+ }
+
public void RemoveAlicesBy(Guid id)
{
using (RoundSyncronizerLock.Lock())

MODIFY
WalletWasabi/Models/Height.cs
WalletWasabi/Models/Height.cs
@@ -328,9 +328,9 @@ namespace WalletWasabi.Models
/// </summary>
/// <param name="value">The Int32 value.</param>
/// <param name="height">The height value to be added.</param>
- public static Height operator +(int value, Height height)
+ public static int operator +(int value, Height height)
{
- return new Height( height.Value + value );
+ return height.Value + value;
}
/// <summary>
@@ -338,9 +338,9 @@ namespace WalletWasabi.Models
/// </summary>
/// <param name="value">The Int32 value.</param>
/// <param name="height">The height value to be substracted from.</param>
- public static Height operator -(int value, Height height)
+ public static int operator -(int value, Height height)
{
- return new Height( value - height.Value );
+ return value - height.Value;
}
/// <summary>
@@ -348,9 +348,9 @@ namespace WalletWasabi.Models
/// </summary>
/// <param name="height">The height value to be added.</param>
/// <param name="value">The Int32 value.</param>
- public static Height operator +(Height height, int value)
+ public static int operator +(Height height, int value)
{
- return new Height( height.Value + value );
+ return height.Value + value;
}
/// <summary>
@@ -358,9 +358,9 @@ namespace WalletWasabi.Models
/// </summary>
/// <param name="height">The height value to be substracted from.</param>
/// <param name="value">The Int32 value.</param>
- public static Height operator -(Height height, int value)
+ public static int operator -(Height height, int value)
{
- return new Height( height.Value - value );
+ return height.Value - value;
}
#endregion

MODIFY
WalletWasabi/Services/CcjCoordinator.cs
WalletWasabi/Services/CcjCoordinator.cs
@@ -107,6 +107,43 @@ namespace WalletWasabi.Services
}
}
+ public async Task ProcessBlockAsync(Block block)
+ {
+ // https://github.com/zkSNACKs/WalletWasabi/issues/145
+ // whenever a block arrives:
+ // go through all its transactions
+ // if a transaction spends a banned output AND it's not CJ output
+ // ban all the outputs of the transaction
+
+ foreach(Transaction tx in block.Transactions)
+ {
+ if (RoundConfig.DosSeverity <= 1) return;
+
+ foreach(TxIn input in tx.Inputs)
+ {
+ OutPoint prevOut = input.PrevOut;
+
+ if(!AnyRunningRoundContainsInput(prevOut, out _))
+ {
+ var found = UtxoReferee.BannedUtxos.SingleOrDefault(x => x.Key == prevOut);
+ if (found.Key != default)
+ {
+ int newSeverity = found.Value.severity + 1;
+ if(RoundConfig.DosSeverity >= newSeverity)
+ {
+ await UtxoReferee.BanUtxosAsync(newSeverity, found.Value.timeOfBan, prevOut);
+ }
+ await UtxoReferee.UnbanAsync(prevOut); // since it's not an UTXO anymore
+ }
+ }
+ else
+ {
+ await UtxoReferee.UnbanAsync(prevOut); // since it's not an UTXO anymore
+ }
+ }
+ }
+ }
+
public void UpdateRoundConfig(CcjRoundConfig roundConfig)
{
RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig);
@@ -119,19 +156,19 @@ namespace WalletWasabi.Services
int runningRoundCount = Rounds.Count(x => x.Status == CcjRoundStatus.Running);
if (runningRoundCount == 0)
{
- var round = new CcjRound(RpcClient, RoundConfig);
+ var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
round.StatusChanged += Round_StatusChangedAsync;
await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);
Rounds.Add(round);
- var round2 = new CcjRound(RpcClient, RoundConfig);
+ var round2 = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
round2.StatusChanged += Round_StatusChangedAsync;
await round2.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);
Rounds.Add(round2);
}
else if(runningRoundCount == 1)
{
- var round = new CcjRound(RpcClient, RoundConfig);
+ var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
round.StatusChanged += Round_StatusChangedAsync;
await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);
Rounds.Add(round);
@@ -159,7 +196,9 @@ namespace WalletWasabi.Services
{
foreach (Alice alice in round.GetAlicesByNot(AliceState.SignedCoinJoin))
{
- await UtxoReferee.BanAliceAsync(alice);
+ // 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());
}
}

MODIFY
WalletWasabi/Services/IndexBuilderService.cs
WalletWasabi/Services/IndexBuilderService.cs
@@ -27,6 +27,9 @@ namespace WalletWasabi.Services
private Dictionary<OutPoint, Script> Bech32UtxoSet { get; }
private List<ActionHistoryHelper> Bech32UtxoSetHistory { get; }
+ public event EventHandler<Block> NewBlock;
+ private void OnNewBlock(Block block) => NewBlock?.Invoke(this, block);
+
private class ActionHistoryHelper
{
public enum Operation
@@ -195,7 +198,7 @@ namespace WalletWasabi.Services
// If stop was requested return.
if (IsRunning == false) return;
- var height = StartingHeight;
+ Height height = StartingHeight;
uint256 prevHash = null;
using (await IndexLock.LockAsync())
{
@@ -207,7 +210,7 @@ namespace WalletWasabi.Services
}
}
- if (blockCount - (int)height <= 100)
+ if (blockCount - height <= 100)
{
isIIB = false;
}
@@ -223,6 +226,11 @@ namespace WalletWasabi.Services
continue;
}
+ if (blockCount - height <= 2)
+ {
+ OnNewBlock(block);
+ }
+
if (prevHash != null)
{
// In case of reorg:

MODIFY
WalletWasabi/Services/UtxoReferee.cs
WalletWasabi/Services/UtxoReferee.cs
@@ -80,15 +80,14 @@ namespace WalletWasabi.Services
}
}
- public async Task BanAliceAsync(Alice alice)
+ public async Task BanUtxosAsync(int severity, DateTimeOffset timeOfBan, params OutPoint[] toBan)
{
var lines = new List<string>();
- foreach(var utxo in alice.Inputs.Select(x=>x.OutPoint))
+ foreach(var utxo in toBan)
{
- DateTimeOffset utcNow = DateTimeOffset.UtcNow;
- if (BannedUtxos.TryAdd(utxo, (1, utcNow)))
+ if (BannedUtxos.TryAdd(utxo, (severity, timeOfBan)))
{
- string line = $"{utcNow.ToString(CultureInfo.InvariantCulture)}:1:{utxo.N}:{utxo.Hash}";
+ string line = $"{timeOfBan.ToString(CultureInfo.InvariantCulture)}:{severity}:{utxo.N}:{utxo.Hash}";
lines.Add(line);
}
}

Finish implementing input banning