Implement WabiSabi coordinator configurability
MODIFY
WalletWasabi.Backend/Global.cs
WalletWasabi.Backend/Global.cs
@@ -14,6 +14,7 @@ using WalletWasabi.CoinJoin.Coordinator.Rounds;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
using WalletWasabi.Services;
+using WalletWasabi.WabiSabi;
namespace WalletWasabi.Backend
{
@@ -22,7 +23,7 @@ namespace WalletWasabi.Backend
public Global(string dataDir)
{
DataDir = dataDir ?? EnvironmentHelpers.GetDataDir(Path.Combine("WalletWasabi", "Backend"));
- HostedServices = new HostedServices();
+ HostedServices = new();
}
public string DataDir { get; }
@@ -36,6 +37,7 @@ namespace WalletWasabi.Backend
public IndexBuilderService IndexBuilderService { get; private set; }
public Coordinator Coordinator { get; private set; }
+ public WabiSabiCoordinator WabiSabiCoordinator { get; private set; }
public Config Config { get; private set; }
@@ -53,6 +55,10 @@ namespace WalletWasabi.Backend
// Make sure P2P works.
await InitializeP2pAsync(config.Network, config.GetBitcoinP2pEndPoint(), cancel);
+ CoordinatorParameters coordinatorParameters = new(DataDir);
+ WabiSabiCoordinator = new(coordinatorParameters);
+ HostedServices.Register(WabiSabiCoordinator, "WabiSabi Coordinator");
+
if (roundConfig.FilePath is { })
{
HostedServices.Register(
@@ -81,8 +87,8 @@ namespace WalletWasabi.Backend
var indexBuilderServiceDir = Path.Combine(DataDir, "IndexBuilderService");
var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{RpcClient.Network}.dat");
var blockNotifier = HostedServices.FirstOrDefault<BlockNotifier>();
- IndexBuilderService = new IndexBuilderService(RpcClient, blockNotifier, indexFilePath);
- Coordinator = new Coordinator(RpcClient.Network, blockNotifier, Path.Combine(DataDir, "CcjCoordinator"), RpcClient, roundConfig);
+ IndexBuilderService = new(RpcClient, blockNotifier, indexFilePath);
+ Coordinator = new(RpcClient.Network, blockNotifier, Path.Combine(DataDir, "CcjCoordinator"), RpcClient, roundConfig);
IndexBuilderService.Synchronize();
Logger.LogInfo($"{nameof(IndexBuilderService)} is successfully initialized and started synchronization.");
@@ -96,7 +102,7 @@ namespace WalletWasabi.Backend
Guard.NotNull(nameof(endPoint), endPoint);
// We have to find it, because it's cloned by the node and not perfectly cloned (event handlers cannot be cloned.)
- P2pNode = new P2pNode(network, endPoint, new MempoolService(), $"/WasabiCoordinator:{Constants.BackendMajorVersion}/");
+ P2pNode = new(network, endPoint, new(), $"/WasabiCoordinator:{Constants.BackendMajorVersion}/");
await P2pNode.ConnectAsync(cancel).ConfigureAwait(false);
HostedServices.Register(new BlockNotifier(TimeSpan.FromSeconds(7), RpcClient, P2pNode), "Block Notifier");
}

ADD
WalletWasabi.Tests/UnitTests/Wabisabi/Backend/ConfigTests.cs
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.WabiSabi;
+using WalletWasabi.WabiSabi.Backend;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class ConfigTests
+ {
+ [Fact]
+ public async Task CreatesConfigAsync()
+ {
+ var workDir = Common.GetWorkDir();
+ await IoHelpers.TryDeleteDirectoryAsync(workDir);
+ CoordinatorParameters coordinatorParameters = new(workDir);
+ using WabiSabiCoordinator coordinator = new(coordinatorParameters);
+ await coordinator.StartAsync(CancellationToken.None);
+
+ Assert.True(File.Exists(Path.Combine(workDir, "WabiSabiConfig.json")));
+
+ await coordinator.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task LoadsConfigAsync()
+ {
+ var workDir = Common.GetWorkDir();
+ await IoHelpers.TryDeleteDirectoryAsync(workDir);
+ CoordinatorParameters coordinatorParameters = new(workDir);
+
+ // Create the config first with default value.
+ using WabiSabiCoordinator coordinator = new(coordinatorParameters);
+ await coordinator.StartAsync(CancellationToken.None);
+ await coordinator.StopAsync(CancellationToken.None);
+
+ // Change the file.
+ var configPath = Path.Combine(workDir, "WabiSabiConfig.json");
+ WabiSabiConfig configChanger = new(configPath);
+ configChanger.LoadOrCreateDefaultFile();
+ var newTarget = 729;
+ configChanger.ConfirmationTarget = newTarget;
+ Assert.NotEqual(newTarget, coordinator.Config.ConfirmationTarget);
+ configChanger.ToFile();
+
+ // Assert the new value is loaded and not the default one.
+ using WabiSabiCoordinator coordinator2 = new(coordinatorParameters);
+ await coordinator2.StartAsync(CancellationToken.None);
+ Assert.Equal(newTarget, coordinator2.Config.ConfirmationTarget);
+ await coordinator2.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task ChecksConfigChangesAsync()
+ {
+ var workDir = Common.GetWorkDir();
+ await IoHelpers.TryDeleteDirectoryAsync(workDir);
+
+ var configChangeMonitoringPeriod = TimeSpan.FromMilliseconds(10);
+ var configChangeAwaitDuration = TimeSpan.FromMilliseconds(100);
+
+ CoordinatorParameters coordinatorParameters = new(workDir) { ConfigChangeMonitoringPeriod = configChangeMonitoringPeriod };
+ using WabiSabiCoordinator coordinator = new(coordinatorParameters);
+ await coordinator.StartAsync(CancellationToken.None);
+
+ var configPath = Path.Combine(workDir, "WabiSabiConfig.json");
+ WabiSabiConfig configChanger = new(configPath);
+ configChanger.LoadOrCreateDefaultFile();
+ var newTarget = 729;
+ configChanger.ConfirmationTarget = newTarget;
+ Assert.NotEqual(newTarget, coordinator.Config.ConfirmationTarget);
+ configChanger.ToFile();
+
+ // Leave time for the config change to be picked up.
+ await Task.Delay(configChangeAwaitDuration);
+ Assert.Equal(newTarget, coordinator.Config.ConfirmationTarget);
+
+ // Do it one more time.
+ newTarget = 372;
+ configChanger.ConfirmationTarget = newTarget;
+ Assert.NotEqual(newTarget, coordinator.Config.ConfirmationTarget);
+ configChanger.ToFile();
+ await Task.Delay(configChangeAwaitDuration);
+ Assert.Equal(newTarget, coordinator.Config.ConfirmationTarget);
+
+ await coordinator.StopAsync(CancellationToken.None);
+ }
+ }
+}

ADD
WalletWasabi.Tests/UnitTests/Wabisabi/Backend/CoordinatorTests.cs
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.WabiSabi;
+using WalletWasabi.WabiSabi.Backend;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class CoordinatorTests
+ {
+ [Fact]
+ public async Task CanLiveAsync()
+ {
+ var workDir = Common.GetWorkDir();
+ await IoHelpers.TryDeleteDirectoryAsync(workDir);
+ CoordinatorParameters coordinatorParameters = new(workDir);
+ using WabiSabiCoordinator coordinator = new(coordinatorParameters);
+ await coordinator.StartAsync(CancellationToken.None);
+ await coordinator.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task CanCancelAsync()
+ {
+ var workDir = Common.GetWorkDir();
+ await IoHelpers.TryDeleteDirectoryAsync(workDir);
+ CoordinatorParameters coordinatorParameters = new(workDir);
+
+ using WabiSabiCoordinator coordinator = new(coordinatorParameters);
+ using CancellationTokenSource cts = new();
+ cts.Cancel();
+ await coordinator.StartAsync(cts.Token);
+ await coordinator.StopAsync(CancellationToken.None);
+
+ using WabiSabiCoordinator coordinator2 = new(coordinatorParameters);
+ using CancellationTokenSource cts2 = new();
+ await coordinator2.StartAsync(cts2.Token);
+ cts2.Cancel();
+ await coordinator2.StopAsync(CancellationToken.None);
+
+ using WabiSabiCoordinator coordinator3 = new(coordinatorParameters);
+ using CancellationTokenSource cts3 = new();
+ var t = coordinator3.StartAsync(cts3.Token);
+ cts3.Cancel();
+ await t;
+ await coordinator3.StopAsync(CancellationToken.None);
+
+ using WabiSabiCoordinator coordinator4 = new(coordinatorParameters);
+ await coordinator4.StartAsync(CancellationToken.None);
+ using CancellationTokenSource cts4 = new();
+ cts4.Cancel();
+ await coordinator4.StopAsync(cts4.Token);
+
+ using WabiSabiCoordinator coordinator5 = new(coordinatorParameters);
+ await coordinator5.StartAsync(CancellationToken.None);
+ using CancellationTokenSource cts5 = new();
+ t = coordinator5.StopAsync(cts5.Token);
+ cts5.Cancel();
+ await t;
+ }
+ }
+}

ADD
WalletWasabi/WabiSabi/Backend/WabiSabiConfig.cs
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WalletWasabi.Bases;
+using WalletWasabi.Helpers;
+
+namespace WalletWasabi.WabiSabi.Backend
+{
+ [JsonObject(MemberSerialization.OptIn)]
+ public class WabiSabiConfig : ConfigBase
+ {
+ public WabiSabiConfig() : base()
+ {
+ }
+
+ public WabiSabiConfig(string filePath) : base(filePath)
+ {
+ }
+
+ [DefaultValue(Constants.OneDayConfirmationTarget)]
+ [JsonProperty(PropertyName = "ConfirmationTarget", DefaultValueHandling = DefaultValueHandling.Populate)]
+ public int ConfirmationTarget { get; set; }
+ }
+}

ADD
WalletWasabi/WabiSabi/CoordinatorParameters.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.WabiSabi
+{
+ public class CoordinatorParameters
+ {
+ public CoordinatorParameters(string dataDir)
+ {
+ DataDir = dataDir;
+ }
+
+ /// <summary>
+ /// The main data directory of the application.
+ /// </summary>
+ public string DataDir { get; }
+
+ /// <summary>
+ /// Configuration of coinjoins can be modified runtime.
+ /// Set how often changes in the configuration file should be monitored.
+ /// </summary>
+ public TimeSpan ConfigChangeMonitoringPeriod { get; init; } = TimeSpan.FromSeconds(10);
+ }
+}

ADD
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
+using Microsoft.Extensions.Hosting;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Logging;
+using WalletWasabi.Services;
+using WalletWasabi.WabiSabi.Backend;
+
+namespace WalletWasabi.WabiSabi
+{
+ public class WabiSabiCoordinator : BackgroundService
+ {
+ public WabiSabiCoordinator(CoordinatorParameters parameters)
+ {
+ DataDir = parameters.DataDir;
+ IoHelpers.EnsureDirectoryExists(DataDir);
+ var configFilePath = Path.Combine(DataDir, "WabiSabiConfig.json");
+ Config = new(configFilePath);
+ Config.LoadOrCreateDefaultFile();
+ Logger.LogInfo("WabiSabi configuration is successfully initialized.");
+
+ ConfigWatcher = new(parameters.ConfigChangeMonitoringPeriod, Config, () => { });
+ }
+
+ public string DataDir { get; }
+ public WabiSabiConfig Config { get; }
+ public ConfigWatcher ConfigWatcher { get; }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await ConfigWatcher.StartAsync(stoppingToken).ConfigureAwait(false);
+ }
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ await base.StopAsync(cancellationToken).ConfigureAwait(false);
+ await ConfigWatcher.StopAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ public override void Dispose()
+ {
+ ConfigWatcher.Dispose();
+ base.Dispose();
+ }
+ }
+}

Implement WabiSabi coordinator configurability