Make sure there's always a round in input registration
MODIFY
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
using NBitcoin;
+using NBitcoin.RPC;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,6 +9,7 @@ using System.Threading.Tasks;
using WalletWasabi.Crypto;
using WalletWasabi.Crypto.Randomness;
using WalletWasabi.Crypto.ZeroKnowledge;
+using WalletWasabi.Tests.UnitTests;
using WalletWasabi.WabiSabi.Backend;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.Rounds;
@@ -67,7 +69,14 @@ namespace WalletWasabi.Tests.Helpers
public static async Task<Arena> CreateAndStartArenaAsync(params Round[] rounds)
{
- Arena arena = new(TimeSpan.FromSeconds(1), rounds.FirstOrDefault()?.Network ?? Network.Main);
+ var mockRpc = new MockRpcClient();
+ mockRpc.OnEstimateSmartFeeAsync = async (target, _) =>
+ await Task.FromResult(new EstimateSmartFeeResponse
+ {
+ Blocks = target,
+ FeeRate = new FeeRate(10m)
+ });
+ Arena arena = new(TimeSpan.FromSeconds(1), rounds.FirstOrDefault()?.Network ?? Network.Main, new WabiSabiConfig(), mockRpc);
foreach (var round in rounds)
{
arena.Rounds.Add(round.Id, round);

ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RoundCreationTests.cs
+using NBitcoin;
+using NBitcoin.RPC;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.WabiSabi.Backend;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class RoundCreationTests
+ {
+ [Fact]
+ public async Task InitializesRoundAsync()
+ {
+ WabiSabiConfig cfg = new();
+ var mockRpc = new MockRpcClient();
+ mockRpc.OnEstimateSmartFeeAsync = async (target, _) =>
+ await Task.FromResult(new EstimateSmartFeeResponse
+ {
+ Blocks = target,
+ FeeRate = new FeeRate(10m)
+ });
+
+ using Arena arena = new Arena(TimeSpan.FromSeconds(1), Network.Main, cfg, mockRpc);
+ Assert.Empty(arena.Rounds);
+ await arena.StartAsync(CancellationToken.None).ConfigureAwait(false);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)).ConfigureAwait(false);
+ Assert.Single(arena.Rounds);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task CreatesRoundIfNoneInputRegistrationAsync()
+ {
+ WabiSabiConfig cfg = new();
+ var mockRpc = new MockRpcClient();
+ mockRpc.OnEstimateSmartFeeAsync = async (target, _) =>
+ await Task.FromResult(new EstimateSmartFeeResponse
+ {
+ Blocks = target,
+ FeeRate = new FeeRate(10m)
+ });
+
+ using Arena arena = new Arena(TimeSpan.FromSeconds(1), Network.Main, cfg, mockRpc);
+ Assert.Empty(arena.Rounds);
+ await arena.StartAsync(CancellationToken.None).ConfigureAwait(false);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)).ConfigureAwait(false);
+ var round = Assert.Single(arena.Rounds);
+
+ round.Value.Phase = Phase.ConnectionConfirmation;
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)).ConfigureAwait(false);
+ Assert.Equal(2, arena.Rounds.Count);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task CreatesRoundIfInBlameInputRegistrationAsync()
+ {
+ WabiSabiConfig cfg = new();
+ var mockRpc = new MockRpcClient();
+ mockRpc.OnEstimateSmartFeeAsync = async (target, _) =>
+ await Task.FromResult(new EstimateSmartFeeResponse
+ {
+ Blocks = target,
+ FeeRate = new FeeRate(10m)
+ });
+
+ using Arena arena = new Arena(TimeSpan.FromSeconds(1), Network.Main, cfg, mockRpc);
+ Assert.Empty(arena.Rounds);
+ await arena.StartAsync(CancellationToken.None).ConfigureAwait(false);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)).ConfigureAwait(false);
+ var round = Assert.Single(arena.Rounds);
+
+ round.Value.Phase = Phase.ConnectionConfirmation;
+ var blame = new Round(round.Value);
+ Assert.Equal(Phase.InputRegistration, blame.Phase);
+ arena.Rounds.Add(blame.Id, blame);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)).ConfigureAwait(false);
+ Assert.Equal(3, arena.Rounds.Count);
+ Assert.Equal(2, arena.Rounds.Where(x => x.Value.Phase == Phase.InputRegistration).Count());
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+ }
+}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
using NBitcoin;
+using NBitcoin.RPC;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -7,6 +8,8 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Bases;
+using WalletWasabi.BitcoinCore.Rpc;
+using WalletWasabi.Crypto.Randomness;
using WalletWasabi.Logging;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.PostRequests;
@@ -17,20 +20,49 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
{
public class Arena : PeriodicRunner
{
- public Arena(TimeSpan period, Network network) : base(period)
+ public Arena(TimeSpan period, Network network, WabiSabiConfig config, IRPCClient rpc) : base(period)
{
Network = network;
+ Config = config;
+ Rpc = rpc;
+ Random = new SecureRandom();
}
public Dictionary<Guid, Round> Rounds { get; } = new();
private object Lock { get; } = new();
public Network Network { get; }
+ public WabiSabiConfig Config { get; }
+ public IRPCClient Rpc { get; }
+ public SecureRandom Random { get; }
- protected override Task ActionAsync(CancellationToken cancel)
+ protected override async Task ActionAsync(CancellationToken cancel)
{
+ var feeRate = (await Rpc.EstimateSmartFeeAsync((int)Config.ConfirmationTarget, EstimateSmartFeeMode.Conservative).ConfigureAwait(false)).FeeRate;
lock (Lock)
{
// Remove timed out alices.
+ TimeoutAlices();
+
+ // Ensure there's at least one non-blame round in inputregistration.
+ CreateRounds(feeRate);
+ }
+ }
+
+ private void CreateRounds(FeeRate feeRate)
+ {
+ if (!Rounds.Values.Any(x => !x.IsBlameRound && x.Phase == Phase.InputRegistration))
+ {
+ RoundParameters roundParams = new(Config, Network, Random, feeRate);
+ Round r = new(roundParams);
+ Rounds.Add(r.Id, r);
+ }
+ }
+
+ private void TimeoutAlices()
+ {
+ // If we cannot timeout alices, then it's ok, don't let the rest fail because of this.
+ try
+ {
foreach (var round in Rounds.Values.Where(x => x.Phase == Phase.InputRegistration))
{
var removedAliceCount = round.RemoveAlices(x => x.Deadline < DateTimeOffset.UtcNow);
@@ -39,8 +71,10 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
Logger.LogInfo($"{removedAliceCount} alices timed out and removed.");
}
}
-
- return Task.CompletedTask;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex);
}
}
@@ -223,5 +257,11 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
}
}
}
+
+ public override void Dispose()
+ {
+ Random.Dispose();
+ base.Dispose();
+ }
}
}

MODIFY
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
@@ -26,7 +26,7 @@ namespace WalletWasabi.WabiSabi
Warden = new(parameters.UtxoWardenPeriod, parameters.PrisonFilePath, Config);
ConfigWatcher = new(parameters.ConfigChangeMonitoringPeriod, Config, () => Logger.LogInfo("WabiSabi configuration has changed."));
- Arena = new(parameters.RoundProgressSteppingPeriod, rpc.Network);
+ Arena = new(parameters.RoundProgressSteppingPeriod, rpc.Network, Config, rpc);
Postman = new(Config, Prison, Arena, Rpc);
}

Make sure there's always a round in input registration