Complete guarding of post requests
ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/ConfirmConnectionTests.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.WabiSabi.Backend;
+using WalletWasabi.WabiSabi.Backend.Banning;
+using WalletWasabi.WabiSabi.Backend.Models;
+using WalletWasabi.WabiSabi.Backend.PostRequests;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Models;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class ConfirmConnectionTests
+ {
+ [Fact]
+ public async Task RoundNotFoundAsync()
+ {
+ MockArena arena = new();
+ arena.OnTryGetRound = _ => null;
+
+ await using PostRequestHandler handler = new(new WabiSabiConfig(), new Prison(), arena, new MockRpcClient());
+ var req = new ConnectionConfirmationRequest(Guid.NewGuid(), Guid.NewGuid(), null!, null!, null!, null!);
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.ConfirmConnection(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.RoundNotFound, ex.ErrorCode);
+ }
+
+ [Fact]
+ public async Task WrongPhaseAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ arena.OnTryGetRound = _ => round;
+
+ var req = new ConnectionConfirmationRequest(round.Id, Guid.NewGuid(), null!, null!, null!, null!);
+ foreach (Phase phase in Enum.GetValues(typeof(Phase)))
+ {
+ if (phase != Phase.InputRegistration && phase != Phase.ConnectionConfirmation)
+ {
+ round.Phase = phase;
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.ConfirmConnection(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task AliceNotFoundAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ arena.OnTryGetRound = _ => round;
+
+ var req = new ConnectionConfirmationRequest(round.Id, Guid.NewGuid(), null!, null!, null!, null!);
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.ConfirmConnection(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.AliceNotFound, ex.ErrorCode);
+ }
+ }
+}

RENAME
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/InputRegistrationTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RegisterInputTests.cs
@@ -16,7 +16,7 @@ using Xunit;
namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
{
- public class InputRegistrationTests
+ public class RegisterInputTests
{
[Fact]
public async Task RoundNotFoundAsync()
@@ -140,7 +140,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
MockArena arena = new();
WabiSabiConfig cfg = new();
var round = WabiSabiFactory.CreateRound(cfg);
- round.Inputs.Add(BitcoinFactory.CreateOutPoint());
+ round.Alices.Add(new Alice(BitcoinFactory.CreateOutPoint()));
Round blameRound = new(round);
arena.OnTryGetRound = _ => blameRound;
@@ -157,7 +157,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
var pair = WabiSabiFactory.CreateInputRoundSignaturePair();
WabiSabiConfig cfg = new();
var round = WabiSabiFactory.CreateRound(cfg);
- round.Inputs.Add(pair.Input);
+ round.Alices.Add(new Alice(pair.Input));
Round blameRound = new(round);
arena.OnTryGetRound = _ => blameRound;
@@ -180,7 +180,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
prison.Punish(pair.Input, Punishment.Banned, Guid.NewGuid());
WabiSabiConfig cfg = new();
var round = WabiSabiFactory.CreateRound(cfg);
- round.Inputs.Add(pair.Input);
+ round.Alices.Add(new Alice(pair.Input));
Round blameRound = new(round);
arena.OnTryGetRound = _ => blameRound;
@@ -243,20 +243,25 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
}
[Fact]
- public async Task InputScriptNotAllowedAsync()
+ public async Task ScriptNotAllowedAsync()
{
MockArena arena = new();
WabiSabiConfig cfg = new();
var round = WabiSabiFactory.CreateRound(cfg);
arena.OnTryGetRound = _ => round;
MockRpcClient rpc = new();
+ using Key key = new();
- rpc.OnGetTxOutAsync = (_, _, _) => new GetTxOutResponse { Confirmations = 1, ScriptPubKeyType = "foo" };
+ rpc.OnGetTxOutAsync = (_, _, _) => new GetTxOutResponse
+ {
+ Confirmations = 1,
+ TxOut = new TxOut(Money.Coins(1), key.PubKey.GetScriptAddress(Network.Main))
+ };
await using PostRequestHandler handler = new(cfg, new Prison(), arena, rpc);
var req = WabiSabiFactory.CreateInputsRegistrationRequest(round);
var ex = await Assert.ThrowsAsync<WabiSabiProtocolException>(async () => await handler.RegisterInputAsync(req));
- Assert.Equal(WabiSabiProtocolErrorCode.InputScriptNotAllowed, ex.ErrorCode);
+ Assert.Equal(WabiSabiProtocolErrorCode.ScriptNotAllowed, ex.ErrorCode);
}
[Fact]

ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RegisterOutputTests.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.WabiSabi.Backend;
+using WalletWasabi.WabiSabi.Backend.Banning;
+using WalletWasabi.WabiSabi.Backend.Models;
+using WalletWasabi.WabiSabi.Backend.PostRequests;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Models;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class RegisterOutputTests
+ {
+ [Fact]
+ public async Task RoundNotFoundAsync()
+ {
+ MockArena arena = new();
+ arena.OnTryGetRound = _ => null;
+
+ await using PostRequestHandler handler = new(new WabiSabiConfig(), new Prison(), arena, new MockRpcClient());
+ var req = new OutputRegistrationRequest(Guid.NewGuid(), null!, null!, null!);
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.RegisterOutput(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.RoundNotFound, ex.ErrorCode);
+ }
+
+ [Fact]
+ public async Task WrongPhaseAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ arena.OnTryGetRound = _ => round;
+ using Key key = new();
+
+ var req = new OutputRegistrationRequest(Guid.NewGuid(), null!, null!, null!);
+ foreach (Phase phase in Enum.GetValues(typeof(Phase)))
+ {
+ if (phase != Phase.OutputRegistration)
+ {
+ round.Phase = phase;
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.RegisterOutput(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task ScriptNotAllowedAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ round.Phase = Phase.OutputRegistration;
+ arena.OnTryGetRound = _ => round;
+ using Key key = new();
+
+ var req = new OutputRegistrationRequest(Guid.NewGuid(), new TxOut(Money.Coins(1), key.PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.Main)), null!, null!);
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.RegisterOutput(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.ScriptNotAllowed, ex.ErrorCode);
+ }
+ }
+}

ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/RemoveInputTests.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.WabiSabi.Backend;
+using WalletWasabi.WabiSabi.Backend.Banning;
+using WalletWasabi.WabiSabi.Backend.Models;
+using WalletWasabi.WabiSabi.Backend.PostRequests;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Models;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class RemoveInputTests
+ {
+ [Fact]
+ public async Task RoundNotFoundAsync()
+ {
+ MockArena arena = new();
+ arena.OnTryGetRound = _ => null;
+
+ await using PostRequestHandler handler = new(new WabiSabiConfig(), new Prison(), arena, new MockRpcClient());
+ var req = new InputsRemovalRequest(Guid.NewGuid(), Guid.NewGuid());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.RemoveInput(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.RoundNotFound, ex.ErrorCode);
+ }
+
+ [Fact]
+ public async Task WrongPhaseAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ arena.OnTryGetRound = _ => round;
+
+ var req = new InputsRemovalRequest(round.Id, Guid.NewGuid());
+ foreach (Phase phase in Enum.GetValues(typeof(Phase)))
+ {
+ if (phase != Phase.InputRegistration)
+ {
+ round.Phase = phase;
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.RemoveInput(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
+ }
+ }
+ }
+ }
+}

ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/SignTransactionTests.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.WabiSabi.Backend;
+using WalletWasabi.WabiSabi.Backend.Banning;
+using WalletWasabi.WabiSabi.Backend.Models;
+using WalletWasabi.WabiSabi.Backend.PostRequests;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Models;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class SignTransactionTests
+ {
+ [Fact]
+ public async Task RoundNotFoundAsync()
+ {
+ MockArena arena = new();
+ arena.OnTryGetRound = _ => null;
+
+ await using PostRequestHandler handler = new(new WabiSabiConfig(), new Prison(), arena, new MockRpcClient());
+ var req = new TransactionSignaturesRequest(Guid.NewGuid(), Guid.NewGuid(), null!);
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.SignTransaction(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.RoundNotFound, ex.ErrorCode);
+ }
+
+ [Fact]
+ public async Task WrongPhaseAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ arena.OnTryGetRound = _ => round;
+
+ var req = new TransactionSignaturesRequest(round.Id, Guid.NewGuid(), null!);
+ foreach (Phase phase in Enum.GetValues(typeof(Phase)))
+ {
+ if (phase != Phase.TransactionSigning)
+ {
+ round.Phase = phase;
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.SignTransaction(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task AliceNotFoundAsync()
+ {
+ MockArena arena = new();
+ WabiSabiConfig cfg = new();
+ var round = WabiSabiFactory.CreateRound(cfg);
+ round.Phase = Phase.TransactionSigning;
+ arena.OnTryGetRound = _ => round;
+
+ var req = new TransactionSignaturesRequest(round.Id, Guid.NewGuid(), null!);
+ await using PostRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient());
+ var ex = Assert.Throws<WabiSabiProtocolException>(() => handler.SignTransaction(req));
+ Assert.Equal(WabiSabiProtocolErrorCode.AliceNotFound, ex.ErrorCode);
+ }
+ }
+}

ADD
WalletWasabi/WabiSabi/Backend/Models/Alice.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.WabiSabi.Backend.Models
+{
+ public class Alice
+ {
+ public Alice(OutPoint outPoint)
+ {
+ OutPoint = outPoint;
+ }
+
+ public Guid Id { get; } = Guid.NewGuid();
+ public OutPoint OutPoint { get; }
+ }
+}

MODIFY
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
WalletWasabi/WabiSabi/Backend/Models/WabiSabiProtocolErrorCode.cs
@@ -13,7 +13,7 @@ namespace WalletWasabi.WabiSabi.Backend.Models
InputSpent,
InputUnconfirmed,
InputImmature,
- InputScriptNotAllowed,
+ ScriptNotAllowed,
WrongRoundSignature,
TooManyInputs,
NotEnoughFunds,
@@ -22,6 +22,7 @@ namespace WalletWasabi.WabiSabi.Backend.Models
NotEnoughWeight,
TooMuchWeight,
InputBanned,
- InputNotWhitelisted
+ InputNotWhitelisted,
+ AliceNotFound
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/PostRequests/PostRequestHandler.cs
WalletWasabi/WabiSabi/Backend/PostRequests/PostRequestHandler.cs
@@ -86,9 +86,9 @@ namespace WalletWasabi.WabiSabi.Backend.PostRequests
{
throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputImmature);
}
- if (!Config.AllowedScriptTypes.Contains(txOutResponse.ScriptPubKeyType))
+ if (!txOutResponse.TxOut.ScriptPubKey.IsScriptType(ScriptType.P2WPKH))
{
- throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputScriptNotAllowed);
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.ScriptNotAllowed);
}
var address = (BitcoinWitPubKeyAddress)txOutResponse.TxOut.ScriptPubKey.GetDestinationAddress(Network);
if (!address.VerifyMessage(round.Hash, inputRoundSignaturePair.RoundSignature))
@@ -97,7 +97,7 @@ namespace WalletWasabi.WabiSabi.Backend.PostRequests
}
inputValueSum += txOutResponse.TxOut.Value;
- if (txOutResponse.ScriptPubKeyType == "witness_v0_keyhash")
+ if (txOutResponse.TxOut.ScriptPubKey.IsScriptType(ScriptType.P2WPKH))
{
// Convert conservative P2WPKH size in virtual bytes to weight units.
inputWeightSum += Constants.P2wpkhInputVirtualSize * 4;
@@ -135,6 +135,15 @@ namespace WalletWasabi.WabiSabi.Backend.PostRequests
DisposeGuard();
using (RunningTasks.RememberWith(RunningRequests))
{
+ if (!Arena.TryGetRound(request.RoundId, out var round))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
+ }
+ if (round.Phase != Phase.InputRegistration)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
+ }
+
throw new NotImplementedException();
}
}
@@ -144,6 +153,19 @@ namespace WalletWasabi.WabiSabi.Backend.PostRequests
DisposeGuard();
using (RunningTasks.RememberWith(RunningRequests))
{
+ if (!Arena.TryGetRound(request.RoundId, out var round))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
+ }
+ if (round.Phase != Phase.InputRegistration && round.Phase != Phase.ConnectionConfirmation)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
+ }
+ if (!round.TryGetAlice(request.AliceId, out var alice))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceNotFound);
+ }
+
throw new NotImplementedException();
}
}
@@ -153,6 +175,19 @@ namespace WalletWasabi.WabiSabi.Backend.PostRequests
DisposeGuard();
using (RunningTasks.RememberWith(RunningRequests))
{
+ if (!Arena.TryGetRound(request.RoundId, out var round))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
+ }
+ if (round.Phase != Phase.OutputRegistration)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
+ }
+ if (!request.Output.ScriptPubKey.IsScriptType(ScriptType.P2WPKH))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.ScriptNotAllowed);
+ }
+
throw new NotImplementedException();
}
}
@@ -162,6 +197,19 @@ namespace WalletWasabi.WabiSabi.Backend.PostRequests
DisposeGuard();
using (RunningTasks.RememberWith(RunningRequests))
{
+ if (!Arena.TryGetRound(request.RoundId, out var round))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
+ }
+ if (round.Phase != Phase.TransactionSigning)
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
+ }
+ if (!round.TryGetAlice(request.AliceId, out var alice))
+ {
+ throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceNotFound);
+ }
+
throw new NotImplementedException();
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
using NBitcoin;
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WalletWasabi.Crypto;
+using WalletWasabi.WabiSabi.Backend.Models;
namespace WalletWasabi.WabiSabi.Backend.Rounds
{
@@ -35,7 +37,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
blameOf.MaxRegistrableWeight)
{
BlameOf = blameOf;
- BlameWhitelist = blameOf.Inputs.ToHashSet();
+ BlameWhitelist = blameOf.Alices.Select(x => x.OutPoint).ToHashSet();
}
public Guid Id { get; } = Guid.NewGuid();
@@ -46,9 +48,15 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
public Money MaxRegistrableAmount { get; }
public uint MinRegistrableWeight { get; }
public uint MaxRegistrableWeight { get; }
- public ISet<OutPoint> Inputs { get; } = new HashSet<OutPoint>();
+ public List<Alice> Alices { get; } = new();
public Round? BlameOf { get; } = null;
public bool IsBlameRound => BlameOf is not null;
public ISet<OutPoint> BlameWhitelist { get; } = new HashSet<OutPoint>();
+
+ public bool TryGetAlice(Guid aliceId, [NotNullWhen(true)] out Alice? alice)
+ {
+ alice = Alices.FirstOrDefault(x => x.Id == aliceId);
+ return alice is not null;
+ }
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/WabiSabiConfig.cs
WalletWasabi/WabiSabi/Backend/WabiSabiConfig.cs
@@ -34,10 +34,6 @@ namespace WalletWasabi.WabiSabi.Backend
[JsonConverter(typeof(TimeSpanJsonConverter))]
public TimeSpan ReleaseUtxoFromPrisonAfter { get; set; } = TimeSpan.FromHours(3);
- [DefaultValueStringSet("[\"witness_v0_keyhash\"]")]
- [JsonProperty(PropertyName = "AllowedScriptTypes", DefaultValueHandling = DefaultValueHandling.Populate)]
- public ISet<string> AllowedScriptTypes { get; set; } = new[] { "witness_v0_keyhash" }.ToHashSet();
-
[DefaultValue(2)]
[JsonProperty(PropertyName = "MaxInputCountByAlice", DefaultValueHandling = DefaultValueHandling.Populate)]
public uint MaxInputCountByAlice { get; set; } = 2;

MODIFY
WalletWasabi/WabiSabi/Models/InputsRemovalRequest.cs
WalletWasabi/WabiSabi/Models/InputsRemovalRequest.cs
@@ -8,11 +8,13 @@ namespace WalletWasabi.WabiSabi.Models
{
public class InputsRemovalRequest
{
- public InputsRemovalRequest(Guid aliceId)
+ public InputsRemovalRequest(Guid aliceId, Guid roundId)
{
AliceId = aliceId;
+ RoundId = roundId;
}
public Guid AliceId { get; }
+ public Guid RoundId { get; }
}
}

Complete guarding of post requests