[WabiSabi] Handle Alice if already registered
MODIFY
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
@@ -37,20 +37,25 @@ namespace WalletWasabi.Tests.Helpers
}
}
- public static IEnumerable<InputRoundSignaturePair> CreateInputRoundSignaturePairs(int count, uint256? roundHash = null)
+ public static InputRoundSignaturePair[] CreateInputRoundSignaturePairs(int count, uint256? roundHash = null)
{
+ List<InputRoundSignaturePair> pairs = new();
for (int i = 0; i < count; i++)
{
- yield return CreateInputRoundSignaturePair(null, roundHash);
+ pairs.Add(CreateInputRoundSignaturePair(null, roundHash));
}
+
+ return pairs.ToArray();
}
- public static IEnumerable<InputRoundSignaturePair> CreateInputRoundSignaturePairs(IEnumerable<Key> keys, uint256? roundHash = null)
+ public static InputRoundSignaturePair[] CreateInputRoundSignaturePairs(IEnumerable<Key> keys, uint256? roundHash = null)
{
+ List<InputRoundSignaturePair> pairs = new();
foreach (var key in keys)
{
- yield return CreateInputRoundSignaturePair(key, roundHash);
+ pairs.Add(CreateInputRoundSignaturePair(key, roundHash));
}
+ return pairs.ToArray();
}
public static Round CreateRound(WabiSabiConfig cfg)
@@ -142,13 +147,13 @@ namespace WalletWasabi.Tests.Helpers
return (ac, wc, ai, wi);
}
- public static (IEnumerable<Credential> amountCredentials, IEnumerable<Credential> weightCredentials) CreateZeroCredentials(Round? round)
+ public static (Credential[] amountCredentials, IEnumerable<Credential> weightCredentials) CreateZeroCredentials(Round? round)
{
(var amClient, var weClient, var amIssuer, var weIssuer) = CreateWabiSabiClientsAndIssuers(round);
return CreateZeroCredentials(amClient, weClient, amIssuer, weIssuer);
}
- private static (IEnumerable<Credential> amountCredentials, IEnumerable<Credential> weightCredentials) CreateZeroCredentials(WabiSabiClient amClient, WabiSabiClient weClient, CredentialIssuer amIssuer, CredentialIssuer weIssuer)
+ private static (Credential[] amountCredentials, IEnumerable<Credential> weightCredentials) CreateZeroCredentials(WabiSabiClient amClient, WabiSabiClient weClient, CredentialIssuer amIssuer, CredentialIssuer weIssuer)
{
var (zeroAmountCredentialRequest, amVal) = amClient.CreateRequestForZeroAmount();
var (zeroWeightCredentialRequest, weVal) = weClient.CreateRequestForZeroAmount();
@@ -156,7 +161,7 @@ namespace WalletWasabi.Tests.Helpers
var weCredResp = weIssuer.HandleRequest(zeroWeightCredentialRequest);
amClient.HandleResponse(amCredResp, amVal);
weClient.HandleResponse(weCredResp, weVal);
- return (amClient.Credentials.ZeroValue.Take(amIssuer.NumberOfCredentials), weClient.Credentials.ZeroValue.Take(weIssuer.NumberOfCredentials));
+ return (amClient.Credentials.ZeroValue.Take(amIssuer.NumberOfCredentials).ToArray(), weClient.Credentials.ZeroValue.Take(weIssuer.NumberOfCredentials));
}
public static (RealCredentialsRequest amountReq, RealCredentialsRequest weightReq) CreateRealCredentialRequests(Round? round = null, Money? amount = null, long? weight = null)

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RegisterInputTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RegisterInputTests.cs
@@ -49,6 +49,78 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
await arena.StopAsync(CancellationToken.None);
}
+ [Fact]
+ public async Task SuccessWithAliceUpdateIntraRoundAsync()
+ {
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(round);
+
+ MockRpcClient rpc = new();
+ using Key key = new();
+
+ rpc.OnGetTxOutAsync = (_, _, _) => new()
+ {
+ Confirmations = 1,
+ ScriptPubKeyType = "witness_v0_keyhash",
+ TxOut = new TxOut(Money.Coins(1), key.PubKey.GetSegwitAddress(Network.Main))
+ };
+
+ var req = WabiSabiFactory.CreateInputsRegistrationRequest(key, round);
+
+ // Make sure an Alice have already been registered with the same input.
+ var preAlice = WabiSabiFactory.CreateAlice(req.InputRoundSignaturePairs);
+ round.Alices.Add(preAlice);
+
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, rpc);
+ var minAliceDeadline = DateTimeOffset.UtcNow + cfg.ConnectionConfirmationTimeout * 0.9;
+ var resp = await handler.RegisterInputAsync(req);
+ var alice = Assert.Single(round.Alices);
+ Assert.NotNull(resp);
+ Assert.NotNull(resp.AmountCredentials);
+ Assert.NotNull(resp.WeightCredentials);
+ Assert.True(minAliceDeadline <= alice.Deadline);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task SuccessWithAliceUpdateCrossRoundAsync()
+ {
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ var anotherRound = WabiSabiFactory.CreateRound(cfg);
+ using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(round, anotherRound);
+
+ MockRpcClient rpc = new();
+ using Key key = new();
+
+ rpc.OnGetTxOutAsync = (_, _, _) => new()
+ {
+ Confirmations = 1,
+ ScriptPubKeyType = "witness_v0_keyhash",
+ TxOut = new TxOut(Money.Coins(1), key.PubKey.GetSegwitAddress(Network.Main))
+ };
+
+ var req = WabiSabiFactory.CreateInputsRegistrationRequest(key, round);
+
+ // Make sure an Alice have already been registered with the same input.
+ var preAlice = WabiSabiFactory.CreateAlice(req.InputRoundSignaturePairs);
+ anotherRound.Alices.Add(preAlice);
+
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, rpc);
+ var minAliceDeadline = DateTimeOffset.UtcNow + cfg.ConnectionConfirmationTimeout * 0.9;
+ var resp = await handler.RegisterInputAsync(req);
+ var alice = Assert.Single(round.Alices);
+ Assert.Empty(anotherRound.Alices);
+ Assert.NotNull(resp);
+ Assert.NotNull(resp.AmountCredentials);
+ Assert.NotNull(resp.WeightCredentials);
+ Assert.True(minAliceDeadline <= alice.Deadline);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+
[Fact]
public async Task RoundNotFoundAsync()
{
@@ -439,5 +511,68 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
await arena.StopAsync(CancellationToken.None);
}
+
+ [Fact]
+ public async Task AliceAlreadyRegisteredIntraRoundAsync()
+ {
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(round);
+
+ MockRpcClient rpc = new();
+ using Key key = new();
+
+ rpc.OnGetTxOutAsync = (_, _, _) => new()
+ {
+ Confirmations = 1,
+ ScriptPubKeyType = "witness_v0_keyhash",
+ TxOut = new TxOut(Money.Coins(1), key.PubKey.GetSegwitAddress(Network.Main))
+ };
+
+ var req = WabiSabiFactory.CreateInputsRegistrationRequest(key, round);
+
+ // Make sure an Alice have already been registered with the same input.
+ var anotherAlice = WabiSabiFactory.CreateAlice(req.InputRoundSignaturePairs);
+ round.Alices.Add(anotherAlice);
+ round.Phase = Phase.ConnectionConfirmation;
+
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, rpc);
+ var ex = await Assert.ThrowsAsync<WabiSabiProtocolException>(async () => await handler.RegisterInputAsync(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task AliceAlreadyRegisteredCrossRoundAsync()
+ {
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ var anotherRound = WabiSabiFactory.CreateRound(cfg);
+ using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(round, anotherRound);
+
+ MockRpcClient rpc = new();
+ using Key key = new();
+
+ rpc.OnGetTxOutAsync = (_, _, _) => new()
+ {
+ Confirmations = 1,
+ ScriptPubKeyType = "witness_v0_keyhash",
+ TxOut = new TxOut(Money.Coins(1), key.PubKey.GetSegwitAddress(Network.Main))
+ };
+
+ var req = WabiSabiFactory.CreateInputsRegistrationRequest(key, round);
+
+ // Make sure an Alice have already been registered with the same input.
+ var preAlice = WabiSabiFactory.CreateAlice(req.InputRoundSignaturePairs);
+ anotherRound.Alices.Add(preAlice);
+ anotherRound.Phase = Phase.ConnectionConfirmation;
+
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, rpc);
+ var ex = await Assert.ThrowsAsync<WabiSabiProtocolException>(async () => await handler.RegisterInputAsync(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.AliceAlreadyRegistered, ex.ErrorCode);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
@@ -25,6 +25,7 @@ namespace WalletWasabi.WabiSabi.Backend.Models
TooMuchWeight,
ScriptNotAllowed,
IncorrectRequestedAmountCredentials,
- WrongCoinjoinSignature
+ WrongCoinjoinSignature,
+ AliceAlreadyRegistered
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Bases;
+using WalletWasabi.Logging;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Crypto.CredentialRequesting;
using WalletWasabi.WabiSabi.Models;
@@ -91,10 +92,33 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchWeight);
}
- return round.RegisterAlice(
+ var resp = round.RegisterAlice(
alice,
zeroAmountCredentialRequests,
zeroWeightCredentialRequests);
+
+ lock (Lock)
+ {
+ foreach (var otherRound in Rounds.Where(x => x.Key != round.Id).Select(x => x.Value))
+ {
+ foreach (var op in alice.Coins.Select(x => x.Outpoint))
+ {
+ try
+ {
+ if (otherRound.RemoveAlices(x => x.Coins.Select(x => x.Outpoint).Contains(op)) > 0)
+ {
+ Logger.LogInfo("Cross round updated Alice registration.");
+ }
+ }
+ catch (WabiSabiProtocolException ex) when (ex.ErrorCode == WabiSabiProtocolErrorCode.WrongPhase)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceAlreadyRegistered);
+ }
+ }
+ }
+ }
+
+ return resp;
}
public void RemoveInput(InputsRemovalRequest request)
@@ -103,7 +127,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
{
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
}
- round.RemoveAlice(request.AliceId);
+ round.RemoveAlices(x => x.Id == request.AliceId);
}
public ConnectionConfirmationResponse ConfirmConnection(ConnectionConfirmationRequest request)

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using WalletWasabi.Crypto;
using WalletWasabi.Crypto.Randomness;
using WalletWasabi.Helpers;
+using WalletWasabi.Logging;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Crypto;
using WalletWasabi.WabiSabi.Crypto.CredentialRequesting;
@@ -85,12 +86,36 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
var weightCredentialResponse = WeightCredentialIssuer.HandleRequest(zeroWeightCredentialRequests);
alice.SetDeadlineRelativeTo(ConnectionConfirmationTimeout);
+ foreach (var op in alice.Coins.Select(x => x.Outpoint))
+ {
+ if (RemoveAlicesNoLock(x => x.Coins.Select(x => x.Outpoint).Contains(op)) > 0)
+ {
+ Logger.LogInfo("Updated Alice registration.");
+ }
+ }
Alices.Add(alice);
return new(alice.Id, amountCredentialResponse, weightCredentialResponse);
}
}
+ public int RemoveAlices(Predicate<Alice> match)
+ {
+ lock (Lock)
+ {
+ return RemoveAlicesNoLock(match);
+ }
+ }
+
+ private int RemoveAlicesNoLock(Predicate<Alice> match)
+ {
+ if (Phase != Phase.InputRegistration)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
+ }
+ return Alices.RemoveAll(match);
+ }
+
public ConnectionConfirmationResponse ConfirmAlice(ConnectionConfirmationRequest request)
{
lock (Lock)
@@ -141,18 +166,6 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
}
}
- public void RemoveAlice(Guid aliceId)
- {
- lock (Lock)
- {
- if (Phase != Phase.InputRegistration)
- {
- throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
- }
- Alices.RemoveAll(x => x.Id == aliceId);
- }
- }
-
public OutputRegistrationResponse RegisterBob(Bob bob, RealCredentialsRequest amountCredentialRequests, RealCredentialsRequest weightCredentialRequests)
{
lock (Lock)

[WabiSabi] Handle Alice if already registered