Add CoinJoinScriptStore
MODIFY
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
@@ -33,7 +33,8 @@ public enum WabiSabiProtocolErrorCode
WrongNumberOfCreds,
CryptoException,
AliceAlreadySignalled,
- AliceAlreadyConfirmedConnection
+ AliceAlreadyConfirmedConnection,
+ AlreadyRegisteredOutputScript
}
public static class WabiSabiProtocolErrorCodeExtension

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
@@ -258,6 +258,11 @@ public partial class Arena : IWabiSabiApiRequestHandler
var credentialAmount = -request.AmountCredentialRequests.Delta;
+ if (CoinJoinScriptStore?.Contains(request.Script) is true)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AlreadyRegisteredOutputScript, $"Round ({request.RoundId}): Already registered output.");
+ }
+
Bob bob = new(request.Script, credentialAmount);
var outputValue = bob.CalculateOutputAmount(round.FeeRate);

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -13,6 +13,7 @@ using WalletWasabi.WabiSabi.Backend.Banning;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Models.MultipartyTransaction;
using WalletWasabi.WabiSabi.Backend.Rounds.CoinJoinStorage;
+using WalletWasabi.WabiSabi.Backend.Statistics;
namespace WalletWasabi.WabiSabi.Backend.Rounds;
@@ -25,7 +26,8 @@ public partial class Arena : PeriodicRunner
IRPCClient rpc,
Prison prison,
InMemoryCoinJoinIdStore inMemoryCoinJoinIdStore,
- CoinJoinTransactionArchiver? archiver = null) : base(period)
+ CoinJoinTransactionArchiver? archiver = null,
+ CoinJoinScriptStore? coinJoinScriptStore = null) : base(period)
{
Network = network;
Config = config;
@@ -34,6 +36,7 @@ public partial class Arena : PeriodicRunner
TransactionArchiver = archiver;
Random = new SecureRandom();
InMemoryCoinJoinIdStore = inMemoryCoinJoinIdStore;
+ CoinJoinScriptStore = coinJoinScriptStore;
}
public HashSet<Round> Rounds { get; } = new();
@@ -44,6 +47,7 @@ public partial class Arena : PeriodicRunner
private Prison Prison { get; }
private SecureRandom Random { get; }
private CoinJoinTransactionArchiver? TransactionArchiver { get; }
+ public CoinJoinScriptStore? CoinJoinScriptStore { get; }
private InMemoryCoinJoinIdStore InMemoryCoinJoinIdStore { get; }
public event EventHandler<Transaction>? CoinJoinBroadcast;
@@ -248,6 +252,7 @@ public partial class Arena : PeriodicRunner
round.LogInfo($"Successfully broadcast the CoinJoin: {coinjoin.GetHash()}.");
InMemoryCoinJoinIdStore.Add(coinjoin.GetHash());
+ CoinJoinScriptStore?.AddRange(coinjoin.Outputs.Select(x => x.ScriptPubKey));
CoinJoinBroadcast?.Invoke(this, coinjoin);
}
else if (round.TransactionSigningTimeFrame.HasExpired)

ADD
WalletWasabi/WabiSabi/Backend/Statistics/CoinJoinScriptStore.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.WabiSabi.Backend.Statistics;
+
+public class CoinJoinScriptStore
+{
+ public CoinJoinScriptStore(IEnumerable<Script> scripts)
+ {
+ Scripts = new HashSet<Script>(scripts);
+ }
+
+ private HashSet<Script> Scripts { get; } = new();
+
+ public void AddRange(IEnumerable<Script> scripts)
+ {
+ foreach (var script in scripts)
+ {
+ Scripts.Add(script);
+ }
+ }
+
+ public bool Contains(Script script)
+ {
+ return Scripts.Contains(script);
+ }
+
+ public static CoinJoinScriptStore LoadFromFile(string filePath)
+ {
+ var scripts = File.Exists(filePath)
+ ? File.ReadAllLines(filePath).Select(x => Script.FromHex(x))
+ : Enumerable.Empty<Script>();
+
+ CoinJoinScriptStore coinJoinScriptStore = new(scripts);
+ return coinJoinScriptStore;
+ }
+}

MODIFY
WalletWasabi/WabiSabi/CoordinatorParameters.cs
WalletWasabi/WabiSabi/CoordinatorParameters.cs
@@ -40,6 +40,8 @@ public class CoordinatorParameters
/// </summary>
public string CoinJoinFeeRateStatStoreFilePath => Path.Combine(CoordinatorDataDir, "CoinJoinFeeRateStatStore.txt");
+ public string CoinJoinScriptStoreFilePath => Path.Combine(CoordinatorDataDir, "CoinJoinScriptStore.txt");
+
/// <summary>
/// Runtime adjustable configuration of the coordinator.
/// </summary>

MODIFY
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
@@ -32,7 +32,19 @@ public class WabiSabiCoordinator : BackgroundService
var inMemoryCoinJoinIdStore = InMemoryCoinJoinIdStore.LoadFromFile(parameters.CoinJoinIdStoreFilePath);
- Arena = new(parameters.RoundProgressSteppingPeriod, rpc.Network, Config, rpc, Warden.Prison, inMemoryCoinJoinIdStore, transactionArchiver);
+ var coinJoinScriptStore = CoinJoinScriptStore.LoadFromFile(parameters.CoinJoinScriptStoreFilePath);
+ IoHelpers.EnsureContainingDirectoryExists(Parameters.CoinJoinScriptStoreFilePath);
+
+ Arena = new(
+ parameters.RoundProgressSteppingPeriod,
+ rpc.Network,
+ Config,
+ rpc,
+ Warden.Prison,
+ inMemoryCoinJoinIdStore,
+ transactionArchiver,
+ coinJoinScriptStore);
+
IoHelpers.EnsureContainingDirectoryExists(Parameters.CoinJoinIdStoreFilePath);
Arena.CoinJoinBroadcast += Arena_CoinJoinBroadcast;
}
@@ -49,14 +61,24 @@ public class WabiSabiCoordinator : BackgroundService
private void Arena_CoinJoinBroadcast(object? sender, Transaction e)
{
- var filePath = Parameters.CoinJoinIdStoreFilePath;
+ var coinJoinIdStoreFilePath = Parameters.CoinJoinIdStoreFilePath;
try
{
- File.AppendAllLines(filePath, new[] { e.GetHash().ToString() });
+ File.AppendAllLines(coinJoinIdStoreFilePath, new[] { e.GetHash().ToString() });
}
catch (Exception ex)
{
- Logger.LogError($"Could not write file {filePath}.", ex);
+ Logger.LogError($"Could not write file {coinJoinIdStoreFilePath}.", ex);
+ }
+
+ var coinJoinScriptStoreFilePath = Parameters.CoinJoinScriptStoreFilePath;
+ try
+ {
+ File.AppendAllLines(coinJoinScriptStoreFilePath, e.Outputs.Select(x => x.ScriptPubKey.ToHex()));
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Could not write file {coinJoinScriptStoreFilePath}.", ex);
}
}

Add CoinJoinScriptStore