Don't try to reregister banned coins when other coins are queued
MODIFY
WalletWasabi.Tests/ModelTests.cs
WalletWasabi.Tests/ModelTests.cs
@@ -129,8 +129,10 @@ namespace WalletWasabi.Tests
var spentOutputs = tx.Inputs.ToTxoRefs().ToArray();
var height = Height.MemPool;
var label = "foo";
+ var bannedUntil = DateTimeOffset.UtcNow;
var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, tx.GetMixin(index), label, txId);
+ coin.BannedUntilUtc = bannedUntil;
var serialized = JsonConvert.SerializeObject(coin);
var deserialized = JsonConvert.DeserializeObject<SmartCoin>(serialized);
@@ -145,6 +147,7 @@ namespace WalletWasabi.Tests
Assert.Equal(coin.SpenderTransactionId, deserialized.SpenderTransactionId);
Assert.Equal(coin.TransactionId, deserialized.TransactionId);
Assert.Equal(coin.SpentOutputs.Length, deserialized.SpentOutputs.Length);
+ Assert.Equal(coin.BannedUntilUtc, deserialized.BannedUntilUtc);
for (int i = 0; i < coin.SpentOutputs.Length; i++)
{
Assert.Equal(coin.SpentOutputs[0], deserialized.SpentOutputs[0]);

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/CcjClientState.cs
WalletWasabi/Models/ChaumianCoinJoin/CcjClientState.cs
@@ -183,10 +183,12 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
var amountSoFar = Money.Zero;
Money amountNeededExceptInputFees = denomination + (feePerOutputs * 2);
foreach (SmartCoin coin in WaitingList
- .Where(y => y.Value <= DateTimeOffset.UtcNow).Select(z => z.Key) // Only if registering coins is already allowed.
+ .Where(x => x.Value <= DateTimeOffset.UtcNow)
+ .Select(x => x.Key) // Only if registering coins is already allowed.
.Where(x => x.Confirmed || x.Label.StartsWith("ZeroLink", StringComparison.Ordinal)) // Where our label contains CoinJoin, CoinJoins can be registered even if not confirmed, our label will likely be CoinJoin only if it was a previous CoinJoin, otherwise the server will refuse us.
- .OrderByDescending(y => y.Amount) // First order by amount.
- .ThenByDescending(z => z.Confirmed)) // Then order by the amount ordered ienumerable by confirmation, so first try to register confirmed coins.
+ .OrderBy(x => x.IsBanned) // First order non-banned coins to first.
+ .ThenByDescending(x => x.Amount) // Then order by amount.
+ .ThenByDescending(x => x.Confirmed)) // Then order by the amount ordered ienumerable by confirmation, so first try to register confirmed coins.
{
coinsToRegister.Add(coin);

MODIFY
WalletWasabi/Models/SmartCoin.cs
WalletWasabi/Models/SmartCoin.cs
@@ -132,9 +132,13 @@ namespace WalletWasabi.Models
[JsonProperty(Order = 11)]
public int Mixin { get; }
+ [JsonProperty(Order = 12)]
+ public DateTimeOffset? BannedUntilUtc { get; set; }
+
public bool SpentOrCoinJoinInProgress => !(SpenderTransactionId is null) || CoinJoinInProgress;
public bool Unspent => SpenderTransactionId is null;
public bool Confirmed => Height != Height.MemPool && Height != Height.Unknown;
+ public bool IsBanned => BannedUntilUtc != null && BannedUntilUtc > DateTimeOffset.UtcNow;
/// <summary>
/// Mixin + 1
@@ -161,6 +165,7 @@ namespace WalletWasabi.Models
RBF = rbf;
CoinJoinInProgress = coinJoinInProgress;
Secret = null;
+ BannedUntilUtc = null;
}
public SmartCoin(Coin coin, TxoRef[] spentOutputs, Height height, bool rbf, int mixin, string label = "", uint256 spenderTransactionId = null, bool coinJoinInProgress = false)

MODIFY
WalletWasabi/Services/CcjClient.cs
WalletWasabi/Services/CcjClient.cs
@@ -502,7 +502,27 @@ namespace WalletWasabi.Services
};
inputProofs.Add(inputProof);
}
- AliceClient aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blind.BlindedData, inputProofs, CcjHostUri, TorSocks5EndPoint);
+
+ AliceClient aliceClient = null;
+ try
+ {
+ aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blind.BlindedData, inputProofs, CcjHostUri, TorSocks5EndPoint);
+ }
+ catch (HttpRequestException ex) when (ex.Message.Contains("Input is banned", StringComparison.InvariantCultureIgnoreCase))
+ {
+ string[] parts = ex.Message.Split(new[] { "Input is banned from participation for ", " minutes: " }, StringSplitOptions.RemoveEmptyEntries);
+ string minutesString = parts[1];
+ int minuteInt = int.Parse(minutesString);
+ string bannedInputString = parts[2].TrimEnd('.');
+ string[] bannedInputStringParts = bannedInputString.Split(':', StringSplitOptions.RemoveEmptyEntries);
+ (uint256 txid, uint index) coinReference = (new uint256(bannedInputStringParts[1]), uint.Parse(bannedInputStringParts[0]));
+ SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
+ if (coin is null) throw new NotSupportedException("This is impossible.");
+ coin.BannedUntilUtc = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(minuteInt);
+
+ Logger.LogWarning<CcjClient>(ex.Message.Split('\n')[1]);
+ return;
+ }
byte[] unblindedSignature = CoordinatorPubKey.UnblindSignature(aliceClient.BlindedOutputSignature, blind.BlindingFactor);

Don't try to reregister banned coins when other coins are queued