Ass some signing stepping tests
MODIFY
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
WalletWasabi.Tests/Helpers/WabiSabiFactory.cs
@@ -68,15 +68,26 @@ namespace WalletWasabi.Tests.Helpers
new InsecureRandom(),
new(100m)));
- public static async Task<Arena> CreateAndStartArenaAsync(WabiSabiConfig? cfg = null, params Round[] rounds)
+ public static async Task<Arena> CreateAndStartArenaAsync(WabiSabiConfig cfg, params Round[] rounds)
+ => await CreateAndStartArenaAsync(cfg, null, rounds);
+
+ public static async Task<Arena> CreateAndStartArenaAsync(WabiSabiConfig? cfg = null, MockRpcClient? mockRpc = null, params Round[] rounds)
{
- var mockRpc = new MockRpcClient();
- mockRpc.OnEstimateSmartFeeAsync = async (target, _) =>
+ mockRpc ??= new MockRpcClient();
+ mockRpc.OnEstimateSmartFeeAsync ??= async (target, _) =>
await Task.FromResult(new EstimateSmartFeeResponse
{
Blocks = target,
FeeRate = new FeeRate(10m)
});
+ mockRpc.OnSendRawTransactionAsync ??= (tx) => tx.GetHash();
+ mockRpc.OnGetTxOutAsync ??= (_, _, _) => new()
+ {
+ Confirmations = 1,
+ ScriptPubKeyType = "witness_v0_keyhash",
+ TxOut = new(Money.Coins(1), Script.Empty)
+ };
+
Arena arena = new(TimeSpan.FromHours(1), rounds.FirstOrDefault()?.Network ?? Network.Main, cfg ?? new WabiSabiConfig(), mockRpc, new Prison());
foreach (var round in rounds)
{

MODIFY
WalletWasabi.Tests/UnitTests/MockRpcClient.cs
WalletWasabi.Tests/UnitTests/MockRpcClient.cs
@@ -16,6 +16,7 @@ namespace WalletWasabi.Tests.UnitTests
public Func<uint256, Task<BlockHeader>> OnGetBlockHeaderAsync { get; set; }
public Func<Task<BlockchainInfo>> OnGetBlockchainInfoAsync { get; set; }
public Func<uint256, Task<VerboseBlockInfo>> OnGetVerboseBlockAsync { get; set; }
+ public Func<Transaction, uint256> OnSendRawTransactionAsync { get; set; }
public Func<Task<MemPoolInfo>> OnGetMempoolInfoAsync { get; set; }
public Func<int, EstimateSmartFeeMode, Task<EstimateSmartFeeResponse>> OnEstimateSmartFeeAsync { get; set; }
public Func<Task<PeerInfo[]>> OnGetPeersInfoAsync { get; set; }
@@ -126,7 +127,8 @@ namespace WalletWasabi.Tests.UnitTests
public Task<uint256> SendRawTransactionAsync(Transaction transaction)
{
- throw new NotImplementedException();
+ var resp = OnSendRawTransactionAsync(transaction);
+ return Task.FromResult(resp);
}
public Task<uint256> SendToAddressAsync(BitcoinAddress address, Money amount, string? commentTx = null, string? commentDest = null, bool subtractFeeFromAmount = false, bool replaceable = false)

ADD
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/StepTransactionSigningTests.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.Tests.Helpers;
+using WalletWasabi.WabiSabi.Backend;
+using WalletWasabi.WabiSabi.Backend.Banning;
+using WalletWasabi.WabiSabi.Backend.PostRequests;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Crypto;
+using WalletWasabi.WabiSabi.Models;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
+{
+ public class StepTransactionSigningTests
+ {
+ [Fact]
+ public async Task EveryoneSignedAsync()
+ {
+ WabiSabiConfig cfg = new()
+ {
+ MaxInputCountByRound = 2,
+ MinInputCountByRoundMultiplier = 0.5
+ };
+ using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg);
+
+ // Create the round.
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ var round = Assert.Single(arena.Rounds).Value;
+
+ // Register Alices.
+ using Key key1 = new();
+ using Key key2 = new();
+ var irreq1 = WabiSabiFactory.CreateInputsRegistrationRequest(key1, round);
+ var irres1 = await arena.RegisterInputAsync(
+ irreq1.RoundId,
+ irreq1.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key1.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature),
+ irreq1.ZeroAmountCredentialRequests,
+ irreq1.ZeroWeightCredentialRequests);
+ var irreq2 = WabiSabiFactory.CreateInputsRegistrationRequest(key2, round);
+ var irres2 = await arena.RegisterInputAsync(
+ irreq2.RoundId,
+ irreq2.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key2.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature),
+ irreq2.ZeroAmountCredentialRequests,
+ irreq2.ZeroWeightCredentialRequests);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.Equal(Phase.ConnectionConfirmation, round.Phase);
+ var alice1 = round.Alices.Single(x => x.Id == irres1.AliceId);
+ var alice2 = round.Alices.Single(x => x.Id == irres2.AliceId);
+
+ // Confirm connections.
+ var ccresps = new List<(ConnectionConfirmationResponse resp, WabiSabiClient amountClient, WabiSabiClient weightClient, Guid aliceId)>();
+ var ugliSolution = false;
+ foreach (var ccreq in WabiSabiFactory.CreateConnectionConfirmationRequests(round, irres1, irres2))
+ {
+ var ccresp = await arena.ConfirmConnectionAsync(ccreq.request);
+ ccresps.Add((ccresp, ccreq.amountClient, ccreq.weightClient, ugliSolution ? irres1.AliceId : irres2.AliceId));
+ ugliSolution = true;
+ ccreq.amountClient.HandleResponse(ccresp.RealAmountCredentials!, ccreq.amountValidation);
+ ccreq.weightClient.HandleResponse(ccresp.RealWeightCredentials!, ccreq.weightValidation);
+ }
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.Equal(Phase.OutputRegistration, round.Phase);
+
+ // Register outputs.
+ foreach (var orreq in WabiSabiFactory.CreateOutputRegistrationRequests(round, ccresps))
+ {
+ var orresp = await arena.RegisterOutputAsync(orreq);
+ }
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.Equal(Phase.TransactionSigning, round.Phase);
+
+ var signedCoinJoin = round.Coinjoin.Clone();
+ var coin1 = alice1.Coins.First();
+ var coin2 = alice2.Coins.First();
+ var idx1 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin1.Outpoint));
+ var idx2 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin2.Outpoint));
+ signedCoinJoin.Sign(key1.GetBitcoinSecret(Network.Main), coin1);
+ var txsigreq1 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx1, signedCoinJoin.Inputs[idx1].WitScript) });
+
+ signedCoinJoin.Sign(key2.GetBitcoinSecret(Network.Main), coin2);
+ var txsigreq2 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx2, signedCoinJoin.Inputs[idx2].WitScript) });
+
+ await arena.SignTransactionAsync(txsigreq1);
+ await arena.SignTransactionAsync(txsigreq2);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.Equal(Phase.TransactionBroadcasting, round.Phase);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task FailsBroadcastAsync()
+ {
+ WabiSabiConfig cfg = new()
+ {
+ MaxInputCountByRound = 2,
+ MinInputCountByRoundMultiplier = 0.5
+ };
+ var mockRpc = new MockRpcClient();
+ mockRpc.OnSendRawTransactionAsync = _ => throw new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null);
+
+ using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc);
+
+ // Create the round.
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ var round = Assert.Single(arena.Rounds).Value;
+
+ // Register Alices.
+ using Key key1 = new();
+ using Key key2 = new();
+ var irreq1 = WabiSabiFactory.CreateInputsRegistrationRequest(key1, round);
+ var irres1 = await arena.RegisterInputAsync(
+ irreq1.RoundId,
+ irreq1.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key1.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature),
+ irreq1.ZeroAmountCredentialRequests,
+ irreq1.ZeroWeightCredentialRequests);
+ var irreq2 = WabiSabiFactory.CreateInputsRegistrationRequest(key2, round);
+ var irres2 = await arena.RegisterInputAsync(
+ irreq2.RoundId,
+ irreq2.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key2.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature),
+ irreq2.ZeroAmountCredentialRequests,
+ irreq2.ZeroWeightCredentialRequests);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.Equal(Phase.ConnectionConfirmation, round.Phase);
+ var alice1 = round.Alices.Single(x => x.Id == irres1.AliceId);
+ var alice2 = round.Alices.Single(x => x.Id == irres2.AliceId);
+
+ // Confirm connections.
+ var ccresps = new List<(ConnectionConfirmationResponse resp, WabiSabiClient amountClient, WabiSabiClient weightClient, Guid aliceId)>();
+ var ugliSolution = false;
+ foreach (var ccreq in WabiSabiFactory.CreateConnectionConfirmationRequests(round, irres1, irres2))
+ {
+ var ccresp = await arena.ConfirmConnectionAsync(ccreq.request);
+ ccresps.Add((ccresp, ccreq.amountClient, ccreq.weightClient, ugliSolution ? irres1.AliceId : irres2.AliceId));
+ ugliSolution = true;
+ ccreq.amountClient.HandleResponse(ccresp.RealAmountCredentials!, ccreq.amountValidation);
+ ccreq.weightClient.HandleResponse(ccresp.RealWeightCredentials!, ccreq.weightValidation);
+ }
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.Equal(Phase.OutputRegistration, round.Phase);
+
+ // Register outputs.
+ foreach (var orreq in WabiSabiFactory.CreateOutputRegistrationRequests(round, ccresps))
+ {
+ var orresp = await arena.RegisterOutputAsync(orreq);
+ }
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.Equal(Phase.TransactionSigning, round.Phase);
+
+ var signedCoinJoin = round.Coinjoin.Clone();
+ var coin1 = alice1.Coins.First();
+ var coin2 = alice2.Coins.First();
+ var idx1 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin1.Outpoint));
+ var idx2 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin2.Outpoint));
+ signedCoinJoin.Sign(key1.GetBitcoinSecret(Network.Main), coin1);
+ var txsigreq1 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx1, signedCoinJoin.Inputs[idx1].WitScript) });
+
+ signedCoinJoin.Sign(key2.GetBitcoinSecret(Network.Main), coin2);
+ var txsigreq2 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx2, signedCoinJoin.Inputs[idx2].WitScript) });
+
+ await arena.SignTransactionAsync(txsigreq1);
+ await arena.SignTransactionAsync(txsigreq2);
+ await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
+ Assert.DoesNotContain(round.Id, arena.Rounds.Keys);
+
+ await arena.StopAsync(CancellationToken.None);
+ }
+ }
+}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -65,7 +65,8 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
{
foreach (var round in Rounds.Values.Where(x =>
x.Phase == Phase.InputRegistration
- && x.IsInputRegistrationEnded(Config.MaxInputCountByRound, Config.GetInputRegistrationTimeout(x))))
+ && x.IsInputRegistrationEnded(Config.MaxInputCountByRound, Config.GetInputRegistrationTimeout(x)))
+ .ToArray())
{
if (round.InputCount < Config.MinInputCountByRound)
{
@@ -80,7 +81,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
private void StepConnectionConfirmationPhase()
{
- foreach (var round in Rounds.Values.Where(x => x.Phase == Phase.ConnectionConfirmation))
+ foreach (var round in Rounds.Values.Where(x => x.Phase == Phase.ConnectionConfirmation).ToArray())
{
if (round.Alices.All(x => x.ConfirmedConnetion))
{
@@ -109,7 +110,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
private void StepOutputRegistrationPhase()
{
- foreach (var round in Rounds.Values.Where(x => x.Phase == Phase.OutputRegistration))
+ foreach (var round in Rounds.Values.Where(x => x.Phase == Phase.OutputRegistration).ToArray())
{
long aliceSum = round.Alices.Sum(x => x.CalculateRemainingAmountCredentials(round.FeeRate));
long bobSum = round.Bobs.Sum(x => x.CredentialAmount);
@@ -154,7 +155,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
private async Task StepTransactionSigningPhaseAsync()
{
- foreach (var round in Rounds.Values.Where(x => x.Phase == Phase.TransactionSigning))
+ foreach (var round in Rounds.Values.Where(x => x.Phase == Phase.TransactionSigning).ToArray())
{
var coinjoin = round.Coinjoin;
var isFullySigned = coinjoin.Inputs.All(x => x.HasWitScript());
@@ -235,7 +236,7 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds
private void TimeoutAlices()
{
- foreach (var round in Rounds.Values.Where(x => !x.IsInputRegistrationEnded(Config.MaxInputCountByRound, Config.GetInputRegistrationTimeout(x))))
+ foreach (var round in Rounds.Values.Where(x => !x.IsInputRegistrationEnded(Config.MaxInputCountByRound, Config.GetInputRegistrationTimeout(x))).ToArray())
{
var removedAliceCount = round.Alices.RemoveAll(x => x.Deadline < DateTimeOffset.UtcNow);
if (removedAliceCount > 0)

Ass some signing stepping tests