Implement timeouts
MODIFY
WalletWasabi.Backend/Global.cs
WalletWasabi.Backend/Global.cs
@@ -79,12 +79,7 @@ namespace WalletWasabi.Backend
if (roundConfig.FilePath != null)
{
RoundConfigWatcher = new ConfigWatcher(RoundConfig);
- RoundConfigWatcher.Start(TimeSpan.FromSeconds(10), OnConfigChangedAsync); // Every 10 seconds check the config
- }
- }
-
- private static async Task OnConfigChangedAsync()
- {
+ RoundConfigWatcher.Start(TimeSpan.FromSeconds(10), async() => {
try
{
Coordinator.FailAllRoundsInInputRegistration();
@@ -95,6 +90,8 @@ namespace WalletWasabi.Backend
{
Logger.LogDebug<ConfigWatcher>(ex);
}
+ }); // Every 10 seconds check the config
+ }
}
private static async Task AssertRpcNodeFullyInitializedAsync()

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

MODIFY
WalletWasabi/ChaumianCoinJoin/Alice.cs
WalletWasabi/ChaumianCoinJoin/Alice.cs
@@ -25,6 +25,8 @@ namespace WalletWasabi.ChaumianCoinJoin
public Money GetChangeAmount(Money denomination, Money coordinatorFee) => OutputSumWithoutCoordinatorFeeAndDenomination - denomination - coordinatorFee;
+ public AliceState State { get; set; }
+
public Alice(Dictionary<OutPoint, TxOut> inputs, Money feeToPay, Script changeOutputScript)
{
Guard.NotNullOrEmpty(nameof(inputs), inputs);
@@ -47,6 +49,8 @@ namespace WalletWasabi.ChaumianCoinJoin
InputSum = inputs.Sum(x => x.Value.Value);
OutputSumWithoutCoordinatorFeeAndDenomination = InputSum - FeeToPay;
+
+ State = AliceState.InputsRegistered;
}
}
}

MODIFY
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
WalletWasabi/ChaumianCoinJoin/CcjRound.cs
@@ -64,8 +64,10 @@ namespace WalletWasabi.ChaumianCoinJoin
}
/// <returns>The next phase or null, if the round is not running.</returns>
- public async Task<CcjRoundPhase?> ExecuteNextPhaseAsync()
+ public async Task<CcjRoundPhase?> ExecuteNextPhaseAsync(TimeSpan timeout, Func<Task> toDoOnSamePhaseDoAfterTimeout)
{
+ CcjRoundPhase? ret = null;
+
using (await RoundSyncronizerLock.LockAsync())
{
try
@@ -102,23 +104,23 @@ namespace WalletWasabi.ChaumianCoinJoin
}
Status = CcjRoundStatus.Running;
- return CcjRoundPhase.InputRegistration;
+ ret = CcjRoundPhase.InputRegistration;
}
else if (Status != CcjRoundStatus.Running) // Failed or succeeded, swallow
{
- return null;
+ ret = null;
}
else if (Phase == CcjRoundPhase.InputRegistration)
{
RoundHash = NBitcoinHelpers.HashOutpoints(Alices.SelectMany(x => x.Inputs).Select(y => y.Key));
Phase = CcjRoundPhase.ConnectionConfirmation;
- return CcjRoundPhase.ConnectionConfirmation;
+ ret = CcjRoundPhase.ConnectionConfirmation;
}
else if (Phase == CcjRoundPhase.ConnectionConfirmation)
{
Phase = CcjRoundPhase.OutputRegistration;
- return CcjRoundPhase.OutputRegistration;
+ ret = CcjRoundPhase.OutputRegistration;
}
else if (Phase == CcjRoundPhase.OutputRegistration)
{
@@ -180,7 +182,7 @@ namespace WalletWasabi.ChaumianCoinJoin
.BuildTransaction(false);
Phase = CcjRoundPhase.Signing;
- return CcjRoundPhase.Signing;
+ ret = CcjRoundPhase.Signing;
}
else
{
@@ -194,6 +196,26 @@ namespace WalletWasabi.ChaumianCoinJoin
throw;
}
}
+
+#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+ Task.Run(async () =>
+ {
+ // Delay asyncronously to the requested timeout.
+ await Task.Delay(timeout);
+
+ var runFailureToDo = false;
+ using (await RoundSyncronizerLock.LockAsync())
+ {
+ runFailureToDo = runFailureToDo = Status == CcjRoundStatus.Running && Phase == ret;
+ }
+ if(runFailureToDo)
+ {
+ await toDoOnSamePhaseDoAfterTimeout(); // outside the lock so to avoid deadlock
+ }
+ });
+#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+
+ return ret;
}
public void Fail()
@@ -204,6 +226,38 @@ namespace WalletWasabi.ChaumianCoinJoin
}
}
+ public int CountAlices()
+ {
+ using (RoundSyncronizerLock.Lock())
+ {
+ return Alices.Count;
+ }
+ }
+
+ public void RemoveAlicesByState(AliceState state)
+ {
+ using (RoundSyncronizerLock.Lock())
+ {
+ Alices.RemoveAll(x => x.State == state);
+ }
+ }
+
+ public int CountBobs()
+ {
+ using (RoundSyncronizerLock.Lock())
+ {
+ return Bobs.Count;
+ }
+ }
+
+ public void UpdateAnonymitySet(int anonymitySet)
+ {
+ using (RoundSyncronizerLock.Lock())
+ {
+ AnonymitySet = anonymitySet;
+ }
+ }
+
public void SetAliceTimeout(Guid uniqueId, TimeSpan timeout)
{
// 1. Find Alice and set its LastSeen propery.

MODIFY
WalletWasabi/ChaumianCoinJoin/CcjRoundConfig.cs
WalletWasabi/ChaumianCoinJoin/CcjRoundConfig.cs
@@ -32,19 +32,35 @@ namespace WalletWasabi.ChaumianCoinJoin
[JsonProperty(PropertyName = "AnonymitySet")]
public int? AnonymitySet { get; private set; }
+ [JsonProperty(PropertyName = "InputRegistrationTimeout")]
+ public long? InputRegistrationTimeout { get; private set; }
+
+ [JsonProperty(PropertyName = "ConnectionConfirmationTimeout")]
+ public long? ConnectionConfirmationTimeout { get; private set; }
+
+ [JsonProperty(PropertyName = "OutputRegistrationTimeout")]
+ public long? OutputRegistrationTimeout { get; private set; }
+
+ [JsonProperty(PropertyName = "SigningTimeout")]
+ public long? SigningTimeout { get; private set; }
+
public CcjRoundConfig(string filePath)
{
SetFilePath(filePath);
}
[JsonConstructor]
- public CcjRoundConfig(Money denomination, int? confirmationTarget, decimal? coordinatorFeePercent, int? anonymitySet)
+ public CcjRoundConfig(Money denomination, int? confirmationTarget, decimal? coordinatorFeePercent, int? anonymitySet, long? inputRegistrationTimeout, long? connectionConfirmationTimeout, long? outputRegistrationTimeout, long? signingTimeout)
{
FilePath = null;
Denomination = Guard.NotNull(nameof(denomination), denomination);
ConfirmationTarget = Guard.NotNull(nameof(confirmationTarget), confirmationTarget);
CoordinatorFeePercent = Guard.NotNull(nameof(coordinatorFeePercent), coordinatorFeePercent);
AnonymitySet = Guard.NotNull(nameof(anonymitySet), anonymitySet);
+ InputRegistrationTimeout = Guard.NotNull(nameof(inputRegistrationTimeout), inputRegistrationTimeout);
+ ConnectionConfirmationTimeout = Guard.NotNull(nameof(connectionConfirmationTimeout), connectionConfirmationTimeout);
+ OutputRegistrationTimeout = Guard.NotNull(nameof(outputRegistrationTimeout), outputRegistrationTimeout);
+ SigningTimeout = Guard.NotNull(nameof(signingTimeout), signingTimeout);
}
/// <inheritdoc />
@@ -67,6 +83,10 @@ namespace WalletWasabi.ChaumianCoinJoin
ConfirmationTarget = 144; // 1 day
CoordinatorFeePercent = 0.1m;
AnonymitySet = 100;
+ InputRegistrationTimeout = 604800; // One week
+ ConnectionConfirmationTimeout = 60;
+ OutputRegistrationTimeout = 60;
+ SigningTimeout = 60;
if (!File.Exists(FilePath))
{
@@ -81,6 +101,10 @@ namespace WalletWasabi.ChaumianCoinJoin
ConfirmationTarget = config.ConfirmationTarget ?? ConfirmationTarget;
CoordinatorFeePercent = config.CoordinatorFeePercent ?? CoordinatorFeePercent;
AnonymitySet = config.AnonymitySet ?? AnonymitySet;
+ InputRegistrationTimeout = config.InputRegistrationTimeout ?? InputRegistrationTimeout;
+ ConnectionConfirmationTimeout = config.ConnectionConfirmationTimeout ?? ConnectionConfirmationTimeout;
+ OutputRegistrationTimeout = config.OutputRegistrationTimeout ?? OutputRegistrationTimeout;
+ SigningTimeout = config.SigningTimeout ?? SigningTimeout;
}
await ToFileAsync();
@@ -115,6 +139,22 @@ namespace WalletWasabi.ChaumianCoinJoin
{
return true;
}
+ if (InputRegistrationTimeout != config.InputRegistrationTimeout)
+ {
+ return true;
+ }
+ if (ConnectionConfirmationTimeout != config.ConnectionConfirmationTimeout)
+ {
+ return true;
+ }
+ if (OutputRegistrationTimeout != config.OutputRegistrationTimeout)
+ {
+ return true;
+ }
+ if (SigningTimeout != config.SigningTimeout)
+ {
+ return true;
+ }
return false;
}

MODIFY
WalletWasabi/Services/CcjCoordinator.cs
WalletWasabi/Services/CcjCoordinator.cs
@@ -38,7 +38,53 @@ namespace WalletWasabi.Services
}
var round = new CcjRound(RpcClient, roundConfig);
- await round.ExecuteNextPhaseAsync();
+ await round.ExecuteNextPhaseAsync(TimeSpan.FromSeconds((long)roundConfig.InputRegistrationTimeout), async () =>
+ {
+ // This will happen outside the lock.
+
+ // Only fail if less two one Alice is registered.
+ // Don't ban anyone, it's ok if they lost connection.
+ int aliceCountAfterInputRegistrationTimeout = round.CountAlices();
+ if (aliceCountAfterInputRegistrationTimeout < 2)
+ {
+ round.Fail();
+ await StartNewRoundAsync(roundConfig);
+ }
+ else
+ {
+ round.UpdateAnonymitySet(aliceCountAfterInputRegistrationTimeout);
+ // Progress to the next phase, which will be ConnectionConfirmation
+ await round.ExecuteNextPhaseAsync(TimeSpan.FromSeconds((long)roundConfig.ConnectionConfirmationTimeout), async () =>
+ {
+ // Only fail if less two one Alice is registered.
+ // Don't ban anyone, it's ok if they lost connection.
+ round.RemoveAlicesByState(AliceState.InputsRegistered);
+ int aliceCountAfterConnectionConfirmationTimeout = round.CountAlices();
+ if (aliceCountAfterConnectionConfirmationTimeout < 2)
+ {
+ round.Fail();
+ await StartNewRoundAsync(roundConfig);
+ }
+ else
+ {
+ round.UpdateAnonymitySet(aliceCountAfterConnectionConfirmationTimeout);
+ // Progress to the next phase, which will be ConnectionConfirmation
+ await round.ExecuteNextPhaseAsync(TimeSpan.FromSeconds((long)roundConfig.OutputRegistrationTimeout), async () =>
+ {
+ // Output registration never fails.
+ // We don't know which Alice to ban.
+ // Therefore proceed to signing, and whichever Alice doesn't sign ban.
+ await round.ExecuteNextPhaseAsync(TimeSpan.FromSeconds((long)roundConfig.SigningTimeout), async () =>
+ {
+ round.Fail();
+ await StartNewRoundAsync(roundConfig);
+ // ToDo: Ban Alices those states are not SignedCoinJoin
+ });
+ });
+ }
+ });
+ }
+ });
Rounds.Add(round);
}
}

Implement timeouts