Allow to distinguish round failing reasons
MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/CoinJoinStateViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/CoinJoinStateViewModel.cs
@@ -11,6 +11,7 @@ using WalletWasabi.Fluent.State;
using WalletWasabi.Fluent.ViewModels.CoinJoinProfiles;
using WalletWasabi.Fluent.ViewModels.Dialogs.Base;
using WalletWasabi.Fluent.ViewModels.Navigation;
+using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Client;
using WalletWasabi.WabiSabi.Client.CoinJoinProgressEvents;
using WalletWasabi.WabiSabi.Client.StatusChangedEvents;
@@ -403,7 +404,9 @@ public partial class CoinJoinStateViewModel : ViewModelBase
switch (coinJoinProgress)
{
case RoundEnded roundEnded:
- CurrentStatus = roundEnded.LastRoundState.WasTransactionBroadcast ? _roundSucceedMessage : _roundFailedMessage;
+ CurrentStatus = roundEnded.LastRoundState.EndRoundState == EndRoundState.TransactionBroadcasted
+ ? _roundSucceedMessage
+ : _roundFailedMessage;
StopCountDown();
break;

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PhaseStepping/StepTransactionSigningTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/PhaseStepping/StepTransactionSigningTests.cs
@@ -86,7 +86,7 @@ public class StepTransactionSigningTests
await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
Assert.DoesNotContain(round, arena.Rounds.Where(x => x.Phase != Phase.Ended));
Assert.Equal(Phase.Ended, round.Phase);
- Assert.False(round.WasTransactionBroadcast);
+ Assert.Equal(EndRoundState.TransactionBroadcastFailed, round.EndRoundState);
Assert.Empty(prison.GetInmates());
await arena.StopAsync(CancellationToken.None);
@@ -126,7 +126,7 @@ public class StepTransactionSigningTests
await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
Assert.DoesNotContain(round, arena.Rounds.Where(x => x.Phase != Phase.Ended));
Assert.Equal(Phase.Ended, round.Phase);
- Assert.False(round.WasTransactionBroadcast);
+ Assert.Equal(EndRoundState.TransactionBroadcastFailed, round.EndRoundState);
// There should be no inmate, because we aren't punishing spenders with banning
// as there's no reason to ban already spent UTXOs,
@@ -167,7 +167,7 @@ public class StepTransactionSigningTests
await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));
Assert.DoesNotContain(round, arena.Rounds.Where(x => x.Phase != Phase.Ended));
Assert.Equal(Phase.Ended, round.Phase);
- Assert.False(round.WasTransactionBroadcast);
+ Assert.Equal(EndRoundState.AbortedNoEnoughAlicesSigned, round.EndRoundState);
Assert.Empty(arena.Rounds.Where(x => x is BlameRound));
Assert.Contains(aliceClient2.SmartCoin.OutPoint, prison.GetInmates().Select(x => x.Utxo));

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
@@ -40,7 +40,7 @@ public partial class Arena : IWabiSabiApiRequestHandler
{
var round = GetRound(request.RoundId);
- var registeredCoins = Rounds.Where(x => !(x.Phase == Phase.Ended && !x.WasTransactionBroadcast))
+ var registeredCoins = Rounds.Where(x => !(x.Phase == Phase.Ended && x.EndRoundState != EndRoundState.TransactionBroadcasted))
.SelectMany(r => r.Alices.Select(a => a.Coin));
if (registeredCoins.Any(x => x.Outpoint == coin.Outpoint))

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -107,7 +107,7 @@ public partial class Arena : PeriodicRunner
{
continue;
}
- round.SetPhase(Phase.Ended);
+ round.EndRound(EndRoundState.AbortedNoEnoughAlices);
round.LogInfo($"Not enough inputs ({round.InputCount}) in {nameof(Phase.InputRegistration)} phase. The minimum is ({Config.MinInputCountByRound}).");
}
else if (round.IsInputRegistrationEnded(Config.MaxInputCountByRound))
@@ -118,7 +118,7 @@ public partial class Arena : PeriodicRunner
}
catch (Exception ex)
{
- round.SetPhase(Phase.Ended);
+ round.EndRound(EndRoundState.AbortedWithError);
round.LogError(ex.Message);
}
}
@@ -161,7 +161,7 @@ public partial class Arena : PeriodicRunner
if (round.InputCount < Config.MinInputCountByRound)
{
- round.SetPhase(Phase.Ended);
+ round.EndRound(EndRoundState.AbortedNoEnoughAlices);
round.LogInfo($"Not enough inputs ({round.InputCount}) in {nameof(Phase.ConnectionConfirmation)} phase. The minimum is ({Config.MinInputCountByRound}).");
}
else
@@ -172,7 +172,7 @@ public partial class Arena : PeriodicRunner
}
catch (Exception ex)
{
- round.SetPhase(Phase.Ended);
+ round.EndRound(EndRoundState.AbortedWithError);
round.LogError(ex.Message);
}
}
@@ -205,7 +205,7 @@ public partial class Arena : PeriodicRunner
}
catch (Exception ex)
{
- round.SetPhase(Phase.Ended);
+ round.EndRound(EndRoundState.AbortedWithError);
round.LogError(ex.Message);
}
}
@@ -230,7 +230,8 @@ public partial class Arena : PeriodicRunner
uint256 roundId = round.Id;
FeeRate feeRate = coinjoin.GetFeeRate(spentCoins);
round.LogInfo($"Network Fee: {networkFee.ToString(false, false)} BTC.");
- round.LogInfo($"Network Fee Rate: {feeRate.FeePerK.ToDecimal(MoneyUnit.Satoshi) / 1000} sat/vByte.");
+ round.LogInfo(
+ $"Network Fee Rate: {feeRate.FeePerK.ToDecimal(MoneyUnit.Satoshi) / 1000} sat/vByte.");
round.LogInfo($"Number of inputs: {coinjoin.Inputs.Count}.");
round.LogInfo($"Number of outputs: {coinjoin.Outputs.Count}.");
round.LogInfo($"Serialized Size: {coinjoin.GetSerializedSize() / 1024} KB.");
@@ -240,7 +241,9 @@ public partial class Arena : PeriodicRunner
{
round.LogInfo($"There are {count} occurrences of {value.ToString(true, false)} outputs.");
}
- round.LogInfo($"There are {indistinguishableOutputs.Count(x => x.count == 1)} occurrences of unique outputs.");
+
+ round.LogInfo(
+ $"There are {indistinguishableOutputs.Count(x => x.count == 1)} occurrences of unique outputs.");
// Store transaction.
if (TransactionArchiver is not null)
@@ -250,7 +253,6 @@ public partial class Arena : PeriodicRunner
// Broadcasting.
await Rpc.SendRawTransactionAsync(coinjoin, cancellationToken).ConfigureAwait(false);
- round.WasTransactionBroadcast = true;
var coordinatorScriptPubKey = Config.GetNextCleanCoordinatorScript();
if (round.CoordinatorScript == coordinatorScriptPubKey)
@@ -264,7 +266,8 @@ public partial class Arena : PeriodicRunner
{
if (address == round.CoordinatorScript)
{
- round.LogError($"Coordinator script pub key reuse detected: {round.CoordinatorScript.ToHex()}");
+ round.LogError(
+ $"Coordinator script pub key reuse detected: {round.CoordinatorScript.ToHex()}");
}
else
{
@@ -272,7 +275,7 @@ public partial class Arena : PeriodicRunner
}
}
- round.SetPhase(Phase.Ended);
+ round.EndRound(EndRoundState.TransactionBroadcasted);
round.LogInfo($"Successfully broadcast the coinjoin: {coinjoin.GetHash()}.");
CoinJoinScriptStore?.AddRange(coinjoin.Outputs.Select(x => x.ScriptPubKey));
@@ -280,8 +283,14 @@ public partial class Arena : PeriodicRunner
}
else if (round.TransactionSigningTimeFrame.HasExpired)
{
- throw new TimeoutException($"Round {round.Id}: Signing phase timed out after {round.TransactionSigningTimeFrame.Duration.TotalSeconds} seconds.");
+ throw new TimeoutException(
+ $"Round {round.Id}: Signing phase timed out after {round.TransactionSigningTimeFrame.Duration.TotalSeconds} seconds.");
+ }
}
+ catch (RPCException ex)
+ {
+ round.LogWarning($"Transaction broadcasting failed: '{ex}'.");
+ round.EndRound(EndRoundState.TransactionBroadcastFailed);
}
catch (Exception ex)
{
@@ -327,12 +336,16 @@ public partial class Arena : PeriodicRunner
}
round.Alices.RemoveAll(x => alicesWhoDidntSign.Contains(x));
- round.SetPhase(Phase.Ended);
if (round.InputCount >= Config.MinInputCountByRound)
{
+ round.EndRound(EndRoundState.NotAllAlicesSign);
await CreateBlameRoundAsync(round, cancellationToken).ConfigureAwait(false);
}
+ else
+ {
+ round.EndRound(EndRoundState.AbortedNoEnoughAlicesSigned);
+ }
}
private async Task CreateBlameRoundAsync(Round round, CancellationToken cancellationToken)

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
@@ -9,6 +9,17 @@ using WalletWasabi.WabiSabi.Models.MultipartyTransaction;
namespace WalletWasabi.WabiSabi.Backend.Rounds;
+public enum EndRoundState
+{
+ None,
+ AbortedWithError,
+ AbortedNoEnoughAlices,
+ TransactionBroadcastFailed,
+ TransactionBroadcasted,
+ NotAllAlicesSign,
+ AbortedNoEnoughAlicesSigned
+};
+
public class Round
{
public Round(RoundParameters parameters, WasabiRandom random)
@@ -47,7 +58,7 @@ public class Round
public TimeFrame OutputRegistrationTimeFrame { get; private set; }
public TimeFrame TransactionSigningTimeFrame { get; private set; }
public DateTimeOffset End { get; private set; }
- public bool WasTransactionBroadcast { get; set; }
+ public EndRoundState EndRoundState { get; set; }
public int RemainingInputVsizeAllocation => Parameters.InitialInputVsizeAllocation - (InputCount * Parameters.MaxVsizeAllocationPerAlice);
public RoundParameters Parameters { get; }
@@ -88,6 +99,12 @@ public class Round
}
}
+ public void EndRound(EndRoundState finalState)
+ {
+ SetPhase(Phase.Ended);
+ EndRoundState = finalState;
+ }
+
public virtual bool IsInputRegistrationEnded(int maxInputCount)
{
if (Phase > Phase.InputRegistration)

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
@@ -204,16 +204,24 @@ public class CoinJoinClient
roundState = await RoundStatusUpdater.CreateRoundAwaiter(s => s.Id == roundId && s.Phase == Phase.Ended, cancellationToken).ConfigureAwait(false);
- var wasTxBroadcast = roundState.WasTransactionBroadcast
- ? $"'{roundState.WasTransactionBroadcast}', Coinjoin TxId: ({unsignedCoinJoin.GetHash()})"
- : $"'{roundState.WasTransactionBroadcast}'";
- roundState.LogDebug($"Ended - WasTransactionBroadcast: {wasTxBroadcast}.");
+ var msg = roundState.EndRoundState switch
+ {
+ EndRoundState.TransactionBroadcasted => $"Broadcasted. Coinjoin TxId: ({unsignedCoinJoin.GetHash()})",
+ EndRoundState.TransactionBroadcastFailed => $"Failed to broadcast. Coinjoin TxId: ({unsignedCoinJoin.GetHash()})",
+ EndRoundState.AbortedWithError => "Round abnormally finished.",
+ EndRoundState.AbortedNoEnoughAlices => "Aborted. Not enough participants.",
+ EndRoundState.AbortedNoEnoughAlicesSigned => "Aborted. Not enough participants signed the coinjoin transaction.",
+ EndRoundState.NotAllAlicesSign => "Aborted. Some Alices didn't sign. Go to blame round.",
+ EndRoundState.None => "Unknown.",
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ roundState.LogDebug(msg);
LogCoinJoinSummary(registeredAliceClients, outputTxOuts, unsignedCoinJoin, roundState);
return new CoinJoinResult(
- GoForBlameRound: !roundState.WasTransactionBroadcast,
- SuccessfulBroadcast: roundState.WasTransactionBroadcast,
+ GoForBlameRound: roundState.EndRoundState == EndRoundState.NotAllAlicesSign,
+ SuccessfulBroadcast: roundState.EndRoundState == EndRoundState.TransactionBroadcasted,
RegisteredCoins: registeredAliceClients.Select(a => a.SmartCoin).ToImmutableList(),
RegisteredOutputs: outputTxOuts.Select(o => o.ScriptPubKey).ToImmutableList());
}

MODIFY
WalletWasabi/WabiSabi/Models/RoundState.cs
WalletWasabi/WabiSabi/Models/RoundState.cs
@@ -12,7 +12,7 @@ public record RoundState(uint256 Id,
CredentialIssuerParameters AmountCredentialIssuerParameters,
CredentialIssuerParameters VsizeCredentialIssuerParameters,
Phase Phase,
- bool WasTransactionBroadcast,
+ EndRoundState EndRoundState,
DateTimeOffset InputRegistrationStart,
TimeSpan InputRegistrationTimeout,
MultipartyTransactionState CoinjoinState)
@@ -26,7 +26,7 @@ public record RoundState(uint256 Id,
round.AmountCredentialIssuerParameters,
round.VsizeCredentialIssuerParameters,
round.Phase,
- round.WasTransactionBroadcast,
+ round.EndRoundState,
round.InputRegistrationTimeFrame.StartTime,
round.InputRegistrationTimeFrame.Duration,
round.CoinjoinState.GetStateFrom(stateId)
@@ -39,7 +39,7 @@ public record RoundState(uint256 Id,
AmountCredentialIssuerParameters,
VsizeCredentialIssuerParameters,
Phase,
- WasTransactionBroadcast,
+ EndRoundState,
InputRegistrationStart,
InputRegistrationTimeout,
CoinjoinState.GetStateFrom(skipFromBaseState)

Allow to distinguish round failing reasons