Implement queuecoins and dequeue coins
MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -35,6 +35,7 @@ using WalletWasabi.Crypto;
using WalletWasabi.Helpers;
using Org.BouncyCastle.Math;
using WalletWasabi.WebClients.ChaumianCoinJoin;
+using System.Security;
namespace WalletWasabi.Tests
{
@@ -1581,15 +1582,7 @@ namespace WalletWasabi.Tests
var addr = await rpc.GetNewAddressAsync();
var hash = await rpc.SendToAddressAsync(addr, new Money(0.01m, MoneyUnit.BTC));
var tx = await rpc.GetRawTransactionAsync(hash);
- var index = 0;
- for (int i = 0; i < tx.Outputs.Count; i++)
- {
- var output = tx.Outputs[i];
- if (output.ScriptPubKey == addr.ScriptPubKey)
- {
- index = i;
- }
- }
+ var index = tx.Outputs.GetIndex(addr.ScriptPubKey);
inputsRequest.Inputs = new List<InputProofModel> { new InputProofModel { Input = new OutPoint(hash, index), Proof = "b" } };
httpRequestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await aliceClient.PostInputsAsync(inputsRequest));
@@ -1611,15 +1604,7 @@ namespace WalletWasabi.Tests
hash = await rpc.SendToAddressAsync(witnessAddress, new Money(0.01m, MoneyUnit.BTC));
await rpc.GenerateAsync(1);
tx = await rpc.GetRawTransactionAsync(hash);
- index = 0;
- for (int i = 0; i < tx.Outputs.Count; i++)
- {
- var output = tx.Outputs[i];
- if (output.ScriptPubKey == witnessAddress.ScriptPubKey)
- {
- index = i;
- }
- }
+ index = tx.Outputs.GetIndex(witnessAddress.ScriptPubKey);
inputsRequest.Inputs = new List<InputProofModel> { new InputProofModel { Input = new OutPoint(hash, index), Proof = "b" } };
httpRequestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await aliceClient.PostInputsAsync(inputsRequest));
Assert.StartsWith($"{HttpStatusCode.BadRequest.ToReasonString()}", httpRequestException.Message);
@@ -1711,15 +1696,7 @@ namespace WalletWasabi.Tests
hash = await rpc.SendToAddressAsync(witnessAddress, new Money(0.01m, MoneyUnit.BTC));
await rpc.GenerateAsync(1);
tx = await rpc.GetRawTransactionAsync(hash);
- index = 0;
- for (int i = 0; i < tx.Outputs.Count; i++)
- {
- var output = tx.Outputs[i];
- if (output.ScriptPubKey == witnessAddress.ScriptPubKey)
- {
- index = i;
- }
- }
+ index = tx.Outputs.GetIndex(witnessAddress.ScriptPubKey);
proof = key.SignMessage(inputsRequest.BlindedOutputScriptHex);
inputProofs.Add(new InputProofModel { Input = new OutPoint(hash, index), Proof = proof });
}
@@ -1778,15 +1755,7 @@ namespace WalletWasabi.Tests
hash = await rpc.SendToAddressAsync(witnessAddress, new Money(0.01m, MoneyUnit.BTC));
await rpc.GenerateAsync(1);
tx = await rpc.GetRawTransactionAsync(hash);
- index = 0;
- for (int i = 0; i < tx.Outputs.Count; i++)
- {
- var output = tx.Outputs[i];
- if (output.ScriptPubKey == witnessAddress.ScriptPubKey)
- {
- index = i;
- }
- }
+ index = tx.Outputs.GetIndex(witnessAddress.ScriptPubKey);
scriptBytes = new Key().PubKey.GetSegwitAddress(network).ScriptPubKey.ToBytes();
(BlindingFactor, BlindedData) = blindingKey.PubKey.Blind(scriptBytes);
@@ -2040,18 +2009,7 @@ namespace WalletWasabi.Tests
Assert.NotNull(txHash);
Transaction transaction = await rpc.GetRawTransactionAsync(txHash);
- var outputIndex = 0;
- var hit = false;
- for (int k = 0; k < transaction.Outputs.Count; k++)
- {
- var output = transaction.Outputs[k];
- if (output.ScriptPubKey == inputAddress.ScriptPubKey)
- {
- outputIndex = k;
- hit = true;
- }
- }
- Assert.True(hit);
+ var outputIndex = transaction.Outputs.GetIndex(inputAddress.ScriptPubKey);
OutPoint input = new OutPoint(txHash, outputIndex);
var inputProof = new InputProofModel { Input = input, Proof = key.SignMessage(ByteHelpers.ToHex(blinded.BlindedData)) };
@@ -2221,11 +2179,44 @@ namespace WalletWasabi.Tests
coordinator.UpdateRoundConfig(roundConfig);
coordinator.FailAllRoundsInInputRegistration();
await rpc.GenerateAsync(3); // So to make sure we have enough money.
+ var keyManager = KeyManager.CreateNew(out _, password);
+ var key1 = keyManager.GenerateNewKey("foo", KeyState.Clean, false);
+ var key2 = keyManager.GenerateNewKey("bar", KeyState.Clean, false);
+ var bech1 = key1.GetP2wpkhAddress(network);
+ var bech2 = key2.GetP2wpkhAddress(network);
+ var amount1 = new Money(0.03m, MoneyUnit.BTC);
+ var amount2 = new Money(0.08m, MoneyUnit.BTC);
+ var txid1 = await rpc.SendToAddressAsync(bech1, amount1, replaceable: false);
+ var txid2 = await rpc.SendToAddressAsync(bech2, amount2, replaceable: false);
+ key1.KeyState = KeyState.Used;
+ key2.KeyState = KeyState.Used;
+ var tx1 = await rpc.GetRawTransactionAsync(txid1);
+ var tx2 = await rpc.GetRawTransactionAsync(txid2);
+ await rpc.GenerateAsync(1);
+ var height = await rpc.GetBlockCountAsync();
+ var smartCoin1 = new SmartCoin(txid1, tx1.Outputs.GetIndex(bech1.ScriptPubKey), bech1.ScriptPubKey, amount1, tx1.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
+ var smartCoin2 = new SmartCoin(txid2, tx2.Outputs.GetIndex(bech2.ScriptPubKey), bech2.ScriptPubKey, amount2, tx2.Inputs.Select(x => new TxoRef(x.PrevOut)).ToArray(), height, rbf: false);
- var chaumianClient = new CcjClient(Global.RpcClient.Network, KeyManager.CreateNew(out _, password), new Uri(RegTestFixture.BackendEndPoint));
+ var chaumianClient = new CcjClient(Global.RpcClient.Network, keyManager, new Uri(RegTestFixture.BackendEndPoint));
try
{
chaumianClient.Start();
+
+ Assert.Throws<NotSupportedException>(() => chaumianClient.QueueCoinsToMix(password, smartCoin1, smartCoin2));
+ smartCoin1.Locked = true;
+ smartCoin2.Locked = true;
+ Assert.Throws<SecurityException>(() => chaumianClient.QueueCoinsToMix("asdasdasd", smartCoin1, smartCoin2));
+
+ // Make sure it doesn't throw.
+ await chaumianClient.DequeueCoinsFromMixAsync();
+ await chaumianClient.DequeueCoinsFromMixAsync((smartCoin1.TransactionId, smartCoin1.Index));
+
+ Assert.True(2 == chaumianClient.QueueCoinsToMix(password, smartCoin1, smartCoin2).Count());
+ await chaumianClient.DequeueCoinsFromMixAsync((smartCoin1.TransactionId, 111));
+ await chaumianClient.DequeueCoinsFromMixAsync((smartCoin1.TransactionId, smartCoin1.Index), (smartCoin2.TransactionId, smartCoin2.Index));
+ Assert.True(2 == chaumianClient.QueueCoinsToMix(password, smartCoin1, smartCoin2).Count());
+ await chaumianClient.DequeueCoinsFromMixAsync((smartCoin1.TransactionId, smartCoin1.Index));
+ await chaumianClient.DequeueCoinsFromMixAsync((smartCoin2.TransactionId, smartCoin2.Index));
}
finally
{

MODIFY
WalletWasabi/Extensions/NBitcoinExtensions.cs
WalletWasabi/Extensions/NBitcoinExtensions.cs
@@ -26,6 +26,25 @@ namespace NBitcoin
}
}
+ public static int GetIndex(this TxOutList me, Script script)
+ {
+ var index = -1;
+ for (int i = 0; i < me.Count; i++)
+ {
+ var output = me[i];
+ if(output.ScriptPubKey == script)
+ {
+ index = i;
+ }
+ }
+
+ if(index == -1)
+ {
+ throw new InvalidOperationException("Script not found.");
+ }
+ return index;
+ }
+
public static string ToHex(this IBitcoinSerializable me)
{
return ByteHelpers.ToHex(me.ToBytes());

MODIFY
WalletWasabi/Services/CcjClient.cs
WalletWasabi/Services/CcjClient.cs
@@ -28,22 +28,12 @@ namespace WalletWasabi.Services
private AsyncLock MixLock { get; }
- private List<(SmartCoin coin, ISecret secret, long? roundId)> CoinsToMix { get; }
+ private List<MixCoin> CoinsToMix { get; }
- private IEnumerable<CcjRunningRoundState> _roundStates;
- public IEnumerable<CcjRunningRoundState> RoundStates
- {
- get => _roundStates;
- private set
- {
- if (_roundStates != value)
- {
- _roundStates = value;
- RoundStatesChanged?.Invoke(this, value);
- }
- }
- }
- public event EventHandler<IEnumerable<CcjRunningRoundState>> RoundStatesChanged;
+ private List<CcjClientRound> Rounds { get; }
+ public event EventHandler<CcjClientRound> RoundAdded;
+ public event EventHandler<long> RoundRemoved;
+ public event EventHandler<CcjClientRound> RoundUpdated;
private long _frequentStatusProcessingIfNotMixing;
@@ -65,11 +55,11 @@ namespace WalletWasabi.Services
BobClient = new BobClient(ccjHostUri, torSocks5EndPoint);
SatoshiClient = new SatoshiClient(ccjHostUri, torSocks5EndPoint);
- _roundStates = null;
+ Rounds = new List<CcjClientRound>();
_running = 0;
Stop = new CancellationTokenSource();
_frequentStatusProcessingIfNotMixing = 0;
- CoinsToMix = new List<(SmartCoin, ISecret, long?)>();
+ CoinsToMix = new List<MixCoin>();
MixLock = new AsyncLock();
}
@@ -100,27 +90,29 @@ namespace WalletWasabi.Services
// If stop was requested return.
if (IsRunning == false) return;
- var count = 0;
+ var inputRegMixing = false;
+ var activelyMixing = false;
using (await MixLock.LockAsync())
{
- // if mixing >- connConf: delay = new Random().Next(2, 7);
- var notNullRoundIds = CoinsToMix.Where(x => x.roundId != null).Select(y => y.roundId).Distinct();
- foreach (var roundId in notNullRoundIds)
- {
- count += RoundStates.Count(x => x.RoundId == roundId && x.Phase >= CcjRoundPhase.ConnectionConfirmation);
- }
+ // if mixing >= connConf: delay = new Random().Next(2, 7);
+ activelyMixing = Rounds.Any(x => x.AliceUniqueId != null && x.State.Phase >= CcjRoundPhase.ConnectionConfirmation);
+ inputRegMixing = Rounds.Any(x => x.AliceUniqueId != null);
}
- if (count > 0)
+ if (activelyMixing)
{
var delay = new Random().Next(2, 7);
await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token);
await ProcessStatusAsync();
}
- else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || CoinsToMix.Count != 0)
+ else if (Interlocked.Read(ref _frequentStatusProcessingIfNotMixing) == 1 || inputRegMixing)
{
double rand = double.Parse($"0.{new Random().Next(2, 8)}"); // randomly between every 0.2 * connConfTimeout and 0.8 * connConfTimeout
- int delay = (int)(rand * RoundStates.First(x => x.Phase == CcjRoundPhase.InputRegistration).RegistrationTimeout);
+ int delay;
+ using (await MixLock.LockAsync())
+ {
+ delay = (int)(rand * Rounds.First(x => x.State.Phase == CcjRoundPhase.InputRegistration).State.RegistrationTimeout);
+ }
await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token);
await ProcessStatusAsync();
@@ -155,7 +147,44 @@ namespace WalletWasabi.Services
try
{
IEnumerable<CcjRunningRoundState> states = await SatoshiClient.GetAllRoundStatesAsync();
- RoundStates = states;
+ using (await MixLock.LockAsync())
+ {
+ foreach (CcjRunningRoundState state in states)
+ {
+ CcjClientRound round = Rounds.SingleOrDefault(x => x.State.RoundId == state.RoundId);
+ if (round == null) // It's a new running round.
+ {
+ var r = new CcjClientRound(state);
+ Rounds.Add(r);
+ RoundAdded?.Invoke(this, r);
+ }
+ else
+ {
+ round.State = state;
+ RoundUpdated?.Invoke(this, round);
+ }
+ }
+
+ var roundsToRemove = new List<long>();
+ foreach (CcjClientRound round in Rounds)
+ {
+ CcjRunningRoundState state = states.SingleOrDefault(x => x.RoundId == round.State.RoundId);
+ if (state == null) // The round is not running anymore.
+ {
+ roundsToRemove.Add(round.State.RoundId);
+ }
+ }
+
+ foreach (long roundId in roundsToRemove)
+ {
+ Rounds.RemoveAll(x => x.State.RoundId == roundId);
+ foreach (var coin in CoinsToMix.Where(x => x.RoundId == roundId))
+ {
+ coin.RemoveFromMix();
+ }
+ RoundRemoved?.Invoke(this, roundId);
+ }
+ }
int delay = new Random().Next(0, 7); // delay the response to defend timing attack privacy
await Task.Delay(TimeSpan.FromSeconds(delay), Stop.Token);
@@ -185,13 +214,18 @@ namespace WalletWasabi.Services
Interlocked.Exchange(ref _frequentStatusProcessingIfNotMixing, 0);
}
- public void QueueCoinsToMix(string password, params SmartCoin[] coins)
+ /// <summary>
+ /// Lock the coins before queueing them.
+ /// </summary>
+ public IEnumerable<SmartCoin> QueueCoinsToMix(string password, params SmartCoin[] coins)
{
using (MixLock.Lock())
{
+ var successful = new List<SmartCoin>();
+
foreach (SmartCoin coin in coins)
{
- if (coin.SpentOrLocked)
+ if (coin.SpenderTransactionId != null)
{
continue;
}
@@ -200,42 +234,78 @@ namespace WalletWasabi.Services
{
continue;
}
- if (CoinsToMix.Select(x => x.coin).Contains(coin))
+ if (CoinsToMix.Select(x => x.SmartCoin).Contains(coin))
{
continue;
}
- coin.Locked = true;
- CoinsToMix.Add((coin, secret, null));
+ CoinsToMix.Add(new MixCoin(coin, secret));
+ successful.Add(coin);
}
+
+ return successful;
}
}
+ /// <summary>
+ /// Unlock coin after dequeuing it.
+ /// </summary>
public async Task DequeueCoinsFromMixAsync(params (uint256 transactionId, int index)[] coins)
{
using (await MixLock.LockAsync())
{
- AggregateException aggregateException = null;
- foreach (var coin in coins)
+ List<Exception> exceptions = new List<Exception>();
+
+ foreach (var coinToDequeue in coins)
{
- // if wasn't even queued return
+ MixCoin coinToMix = CoinsToMix.SingleOrDefault(x => x.SmartCoin.TransactionId == coinToDequeue.transactionId && x.SmartCoin.Index == coinToDequeue.index);
+ // if wasn't even queued continue
+ if (coinToMix == null)
+ {
+ continue;
+ }
// if its round is >= connconf: add to aggregateException, continue; cannot dequeue
- // if its round is inputreg, send unconfirm req and depending on the response add to aggregateException, continue; or unqueue coin and remove roundId from its siblings (were registered to the same round)
- // unqueue coin: 1) remove from list, 2) make coin unlocked
+ if (coinToMix.RoundId != null)
+ {
+ CcjClientRound round = Rounds.Single(x => x.State.RoundId == coinToMix.RoundId); // Round must be present and running.
+ if (round.State.Phase >= CcjRoundPhase.ConnectionConfirmation)
+ {
+ exceptions.Add(new NotSupportedException($"Cannot deque coin in {round.State.Phase} phase. Coin: {coinToDequeue.index}:{coinToDequeue.transactionId}."));
+ continue;
}
-
- if (aggregateException != null)
+ else // // if its round is inputreg, send unconfirm req and depending on the response add to aggregateException, continue; or unqueue coin and remove roundId from its siblings (were registered to the same round)
{
- if (aggregateException.InnerExceptions.Count == 1)
+ try
+ {
+ await AliceClient.PostUnConfirmationAsync(round.State.RoundId, (Guid)round.AliceUniqueId); // AliceUniqueId must be there.
+ foreach(var c in CoinsToMix)
{
- throw aggregateException.InnerExceptions.First();
+ if(c.RoundId == round.State.RoundId)
+ {
+ c.RemoveFromMix();
}
- else
+ }
+ }
+ catch (Exception ex)
{
- throw aggregateException;
+ exceptions.Add(ex);
+ continue;
}
}
}
+ coinToMix.RemoveFromMix();
+ CoinsToMix.Remove(coinToMix);
+ }
+
+ if (exceptions.Count == 1)
+ {
+ throw exceptions.Single();
+ }
+ else if (exceptions.Count > 0)
+ {
+ throw new AggregateException(exceptions);
+ }
+ }
}
public async Task StopAsync()
@@ -258,7 +328,7 @@ namespace WalletWasabi.Services
try
{
// Try to dequeue all coins at last. This should be done manually, and prevent the closing of the software if unsuccessful.
- await DequeueCoinsFromMixAsync(CoinsToMix.Select(x => (x.coin.TransactionId, x.coin.Index)).ToArray());
+ await DequeueCoinsFromMixAsync(CoinsToMix.Select(x => (x.SmartCoin.TransactionId, x.SmartCoin.Index)).ToArray());
}
catch (Exception ex)
{

Implement queuecoins and dequeue coins