Handle a few errors
MODIFY
WalletWasabi/WabiSabi/Client/AliceClient.cs
WalletWasabi/WabiSabi/Client/AliceClient.cs
@@ -31,7 +31,7 @@ namespace WalletWasabi.WabiSabi.Client
private ArenaClient ArenaClient { get; }
public Coin Coin { get; }
private FeeRate FeeRate { get; }
- private BitcoinSecret BitcoinSecret { get; }
+ public BitcoinSecret BitcoinSecret { get; }
public IEnumerable<Credential> IssuedAmountCredentials { get; private set; }
public IEnumerable<Credential> IssuedVsizeCredentials { get; private set; }

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
using NBitcoin;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Blockchain.Keys;
using WalletWasabi.Blockchain.TransactionOutputs;
+using WalletWasabi.Crypto;
using WalletWasabi.Crypto.Randomness;
using WalletWasabi.Logging;
using WalletWasabi.WabiSabi.Backend.Models;
@@ -34,7 +36,7 @@ namespace WalletWasabi.WabiSabi.Client
Keymanager = keymanager;
RoundStatusUpdater = roundStatusUpdater;
SecureRandom = new SecureRandom();
- Coins = coins;
+ Coins = coins.ToImmutableArray();
}
private IEnumerable<SmartCoin> Coins { get; set; }
@@ -44,10 +46,13 @@ namespace WalletWasabi.WabiSabi.Client
public KeyManager Keymanager { get; }
private RoundStateUpdater RoundStatusUpdater { get; }
+ private const int MaxInputsRegistrableByWallet = 7; // how many
+
public async Task<bool> StartCoinJoinAsync(CancellationToken cancellationToken)
{
var currentRoundState = await RoundStatusUpdater.CreateRoundAwaiter(roundState => roundState.Phase == Phase.InputRegistration, cancellationToken).ConfigureAwait(false);
+
// This should be roughly log(#inputs), it could be set slightly
// higher if more inputs are observed but that involves trusting the
// coordinator with those values. Therefore, conservatively set this
@@ -78,21 +83,25 @@ namespace WalletWasabi.WabiSabi.Client
{
var constructionState = roundState.Assert<ConstructionState>();
+ // Register coins.
+ var aliceClients = await RegisterCoinsAsync(CreateAliceClients(roundState), cancellationToken).ConfigureAwait(false);
+ if (!aliceClients.Any())
+ {
+ throw new InvalidOperationException("There is no available alices to participate with.");
+ }
+
// Calculate outputs values
- var outputValues = DecomposeAmounts(roundState.FeeRate, roundState.CoinjoinState.Parameters.AllowedOutputAmounts.Min);
+ var registeredCoins = aliceClients.Select(x => x.Coin);
+ var outputValues = DecomposeAmounts(registeredCoins, roundState.FeeRate, roundState.CoinjoinState.Parameters.AllowedOutputAmounts.Min);
// Get all locked internal keys we have and assert we have enough.
Keymanager.AssertLockedInternalKeysIndexed(howMany: outputValues.Count());
var allLockedInternalKeys = Keymanager.GetKeys(x => x.IsInternal && x.KeyState == KeyState.Locked);
var outputTxOuts = outputValues.Zip(allLockedInternalKeys, (amount, hdPubKey) => new TxOut(amount, hdPubKey.P2wpkhScript));
- List<AliceClient> aliceClients = CreateAliceClients(roundState);
- DependencyGraph dependencyGraph = DependencyGraph.ResolveCredentialDependencies(aliceClients.Select(a => a.Coin), outputTxOuts, roundState.FeeRate, roundState.MaxVsizeAllocationPerAlice);
+ DependencyGraph dependencyGraph = DependencyGraph.ResolveCredentialDependencies(registeredCoins, outputTxOuts, roundState.FeeRate, roundState.MaxVsizeAllocationPerAlice);
DependencyGraphTaskScheduler scheduler = new(dependencyGraph);
- // Register coins.
- await RegisterCoinsAsync(aliceClients, cancellationToken).ConfigureAwait(false);
-
// Confirm coins.
await scheduler.StartConfirmConnectionsAsync(aliceClients, dependencyGraph, roundState.ConnectionConfirmationTimeout, RoundStatusUpdater, cancellationToken).ConfigureAwait(false);
@@ -122,14 +131,14 @@ namespace WalletWasabi.WabiSabi.Client
await SignTransactionAsync(aliceClients, unsignedCoinJoin, cancellationToken).ConfigureAwait(false);
var finalRoundState = await RoundStatusUpdater.CreateRoundAwaiter(s => s.Id == roundState.Id && s.Phase == Phase.Ended, cancellationToken).ConfigureAwait(false);
-
return finalRoundState.WasTransactionBroadcast;
}
private List<AliceClient> CreateAliceClients(RoundState roundState)
{
List<AliceClient> aliceClients = new();
- foreach (var coin in Coins.Select(x => x.Coin))
+ var coins = SelectCoinsForRound(Coins, roundState).Select(x => x.Coin);
+ foreach (var coin in coins)
{
var aliceArenaClient = new ArenaClient(
roundState.CreateAmountCredentialClient(SecureRandom),
@@ -138,20 +147,25 @@ namespace WalletWasabi.WabiSabi.Client
var hdKey = Keymanager.GetSecrets(Kitchen.SaltSoup(), coin.ScriptPubKey).Single();
var secret = hdKey.PrivateKey.GetBitcoinSecret(Keymanager.GetNetwork());
+ if (hdKey.PrivateKey.PubKey.WitHash.ScriptPubKey != coin.ScriptPubKey)
+ {
+ throw new InvalidOperationException("The key cannot generate the utxo scriptpubkey. This could happen if the wallet password is not the correct one.");
+ }
aliceClients.Add(new AliceClient(roundState.Id, aliceArenaClient, coin, roundState.FeeRate, secret));
}
return aliceClients;
}
- private async Task RegisterCoinsAsync(IEnumerable<AliceClient> aliceClients, CancellationToken cancellationToken)
+ private async Task<ImmutableArray<AliceClient>> RegisterCoinsAsync(IEnumerable<AliceClient> aliceClients, CancellationToken cancellationToken)
{
- async Task RegisterInputTask(AliceClient aliceClient)
+ async Task<AliceClient?> RegisterInputTask(AliceClient aliceClient)
{
var smartCoin = Coins.Single(x => x.Coin == aliceClient.Coin);
try
{
await aliceClient.RegisterInputAsync(cancellationToken).ConfigureAwait(false);
smartCoin.CoinJoinInProgress = true;
+ return aliceClient;
}
catch (System.Net.Http.HttpRequestException ex)
{
@@ -161,28 +175,42 @@ namespace WalletWasabi.WabiSabi.Client
{
case WabiSabiProtocolErrorCode.InputSpent:
smartCoin.SpentAccordingToBackend = true;
+ Logger.LogInfo($"{smartCoin.Coin.Outpoint} is spent according to the backend. The wallet is not fully synchronized or corrupted.");
break;
case WabiSabiProtocolErrorCode.InputBanned:
smartCoin.BannedUntilUtc = DateTimeOffset.UtcNow.AddDays(1);
smartCoin.SetIsBanned();
+ Logger.LogInfo($"{smartCoin.Coin.Outpoint} is banned.");
+ break;
+ case WabiSabiProtocolErrorCode.InputNotWhitelisted:
+ smartCoin.SpentAccordingToBackend = false;
+ Logger.LogInfo($"{smartCoin.Coin.Outpoint} cannot be registered in the blame round.");
break;
- default:
- return;
+ case WabiSabiProtocolErrorCode.AliceAlreadyRegistered:
+ Logger.LogInfo($"{smartCoin.Coin.Outpoint} was already registered.");
+ return aliceClient;
+ case WabiSabiProtocolErrorCode.WrongPhase:
+ return null; // The coin didn't get it and arrived too late to the party.
}
-
}
- throw;
+ Logger.LogInfo($"{smartCoin.Coin.Outpoint} registration failed with {ex}.");
+ return null;
}
}
var registerRequests = aliceClients.Select(RegisterInputTask);
await Task.WhenAll(registerRequests).ConfigureAwait(false);
+
+ return registerRequests
+ .Where(x => x.Result is not null)
+ .Select(x => x.Result!)
+ .ToImmutableArray();
}
- private IEnumerable<Money> DecomposeAmounts(FeeRate feeRate, Money minimumOutputAmount)
+ private IEnumerable<Money> DecomposeAmounts(IEnumerable<Coin> coins, FeeRate feeRate, Money minimumOutputAmount)
{
GreedyDecomposer greedyDecomposer = new(StandardDenomination.Values.Where(x => x >= minimumOutputAmount));
- var sum = Coins.Sum(c => c.EffectiveValue(feeRate));
+ var sum = coins.Sum(c => c.EffectiveValue(feeRate));
return greedyDecomposer.Decompose(sum, feeRate.GetFee(31));
}
@@ -233,5 +261,14 @@ namespace WalletWasabi.WabiSabi.Client
var readyRequests = aliceClients.Select(ReadyToSignTask);
await Task.WhenAll(readyRequests).ConfigureAwait(false);
}
+
+ private ImmutableList<SmartCoin> SelectCoinsForRound(IEnumerable<SmartCoin> coins, RoundState roundState) =>
+ coins
+ .Where(x => roundState.CoinjoinState.Parameters.AllowedInputAmounts.Contains(x.Amount) )
+ .Where(x => roundState.CoinjoinState.Parameters.AllowedInputTypes.Any(t => x.ScriptPubKey.IsScriptType(t)))
+ .OrderBy(x => x.HdPubKey.AnonymitySet)
+ .ThenByDescending(x => x.Amount)
+ .Take(MaxInputsRegistrableByWallet)
+ .ToImmutableList();
}
}

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
@@ -20,8 +20,6 @@ namespace WalletWasabi.WabiSabi.Client
{
public class CoinJoinManager : BackgroundService
{
- private const int MaxInputsRegistrableByWallet = 7; // how many
-
public CoinJoinManager(WalletManager walletManager, RoundStateUpdater roundStatusUpdater, HttpClientFactory backendHttpClientFactory, ServiceConfiguration serviceConfiguration)
{
WalletManager = walletManager;
@@ -39,31 +37,19 @@ namespace WalletWasabi.WabiSabi.Client
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var trackedWallets = new Dictionary<string, WalletTrackingData>();
- Task<RoundState> roundUpdateTask = RoundStatusUpdater.CreateRoundAwaiter(roundState => roundState.Phase == Phase.InputRegistration, stoppingToken);
- var currentRoundState = await roundUpdateTask.ConfigureAwait(false);
while (!stoppingToken.IsCancellationRequested)
{
- var delayTask = Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
- var completedTask = await Task.WhenAny(
- roundUpdateTask,
- delayTask
- ).ConfigureAwait(false);
-
- if (completedTask == roundUpdateTask)
- {
- currentRoundState = await roundUpdateTask.ConfigureAwait(false);
- roundUpdateTask = RoundStatusUpdater.CreateRoundAwaiter(roundState => roundState.Phase == Phase.InputRegistration, stoppingToken);
- }
- Logger.LogInfo($"Current round: {currentRoundState.Id} - ({currentRoundState.Phase} ({currentRoundState.BlameOf} )");
+ await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken).ConfigureAwait(false);
var mixableWallets = GetMixableWallets();
var openedWallets = mixableWallets.Where(x => !trackedWallets.ContainsKey(x.Key));
var closedWallets = trackedWallets.Where(x => !mixableWallets.ContainsKey(x.Key));
foreach (var openedWallet in openedWallets.Select(x => x.Value))
{
- var coins = SelectCoinsForWallet(openedWallet, currentRoundState);
- var coinjoinClient = new CoinJoinClient(ArenaRequestHandler, coins, openedWallet.Kitchen, openedWallet.KeyManager, RoundStatusUpdater);
+ var coinCandidates = openedWallet.Coins.Available().Confirmed().Where(x => x.HdPubKey.AnonymitySet < ServiceConfiguration.GetMixUntilAnonymitySetValue());
+
+ var coinjoinClient = new CoinJoinClient(ArenaRequestHandler, coinCandidates, openedWallet.Kitchen, openedWallet.KeyManager, RoundStatusUpdater);
var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
var coinjoinTask = coinjoinClient.StartCoinJoinAsync(cts.Token);
@@ -93,6 +79,11 @@ namespace WalletWasabi.WabiSabi.Client
}
else if (finishedCoinJoinTask.IsFaulted)
{
+ if (finishedCoinJoinTask.Exception?.InnerException is InvalidOperationException ioe)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
+ continue;
+ }
Logger.LogError(finishedCoinJoinTask.Exception!);
}
else if (finishedCoinJoinTask.IsCanceled)
@@ -103,16 +94,6 @@ namespace WalletWasabi.WabiSabi.Client
}
}
- private ImmutableList<SmartCoin> SelectCoinsForWallet(Wallet wallet, RoundState roundState) =>
- wallet.Coins.Available().Confirmed()
- .Where(x => x.HdPubKey.AnonymitySet < ServiceConfiguration.GetMixUntilAnonymitySetValue())
- .Where(x => roundState.CoinjoinState.Parameters.AllowedInputAmounts.Contains(x.Amount) )
- .Where(x => roundState.CoinjoinState.Parameters.AllowedInputTypes.Any(t => x.ScriptPubKey.IsScriptType(t)))
- .OrderBy(x => x.HdPubKey.AnonymitySet)
- .ThenByDescending(x => x.Amount)
- .Take(MaxInputsRegistrableByWallet)
- .ToImmutableList();
-
private ImmutableDictionary<string, Wallet> GetMixableWallets() =>
WalletManager.GetWallets()
.Where(x => x.State == WalletState.Started) // Only running wallets

Handle a few errors