Implement client side
MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -1958,7 +1958,7 @@ namespace WalletWasabi.Tests
Script script = new Key().PubKey.GetSegwitAddress(network).ScriptPubKey;
blindedData = requester.BlindScript(round.Signer.Key.PubKey, round.Signer.R.PubKey, script);
- using (var aliceClient = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blindedData, new InputProofModel[] { new InputProofModel { Input = coin.Outpoint.ToTxoRef(), Proof = key.SignCompact(blindedData) } }, baseUri))
+ using (var aliceClient = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blindedData, Enumerable.Empty<uint256>(), new InputProofModel[] { new InputProofModel { Input = coin.Outpoint.ToTxoRef(), Proof = key.SignCompact(blindedData) } }, baseUri))
{
Assert.NotNull(aliceClient.BlindedOutputSignature);
Assert.NotEqual(Guid.Empty, aliceClient.UniqueId);
@@ -1998,7 +1998,7 @@ namespace WalletWasabi.Tests
Assert.Equal($"{HttpStatusCode.Gone.ToReasonString()}\nRound is not running.", httpRequestException.Message);
}
- using (var aliceClient = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blindedData, new InputProofModel[] { new InputProofModel { Input = coin.Outpoint.ToTxoRef(), Proof = key.SignCompact(blindedData) } }, baseUri))
+ using (var aliceClient = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blindedData, Enumerable.Empty<uint256>(), new InputProofModel[] { new InputProofModel { Input = coin.Outpoint.ToTxoRef(), Proof = key.SignCompact(blindedData) } }, baseUri))
{
Assert.NotNull(aliceClient.BlindedOutputSignature);
Assert.NotEqual(Guid.Empty, aliceClient.UniqueId);
@@ -2058,8 +2058,8 @@ namespace WalletWasabi.Tests
var input1 = new OutPoint(hash1, index1);
var input2 = new OutPoint(hash2, index2);
- using (var aliceClient1 = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blinded1, new InputProofModel[] { new InputProofModel { Input = input1.ToTxoRef(), Proof = key1.SignCompact(blinded1) } }, baseUri))
- using (var aliceClient2 = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blinded2, new InputProofModel[] { new InputProofModel { Input = input2.ToTxoRef(), Proof = key2.SignCompact(blinded2) } }, baseUri))
+ using (var aliceClient1 = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blinded1, Enumerable.Empty<uint256>(), new InputProofModel[] { new InputProofModel { Input = input1.ToTxoRef(), Proof = key1.SignCompact(blinded1) } }, baseUri))
+ using (var aliceClient2 = await AliceClient.CreateNewAsync(network, new Key().PubKey.GetAddress(network), blinded2, Enumerable.Empty<uint256>(), new InputProofModel[] { new InputProofModel { Input = input2.ToTxoRef(), Proof = key2.SignCompact(blinded2) } }, baseUri))
{
Assert.Equal(aliceClient2.RoundId, aliceClient1.RoundId);
Assert.NotEqual(aliceClient2.UniqueId, aliceClient1.UniqueId);
@@ -2084,8 +2084,8 @@ namespace WalletWasabi.Tests
using (var bobClient1 = new BobClient(baseUri))
using (var bobClient2 = new BobClient(baseUri))
{
- await bobClient1.PostOutputAsync(aliceClient1.RoundId, outputAddress1, unblindedSignature1);
- await bobClient2.PostOutputAsync(aliceClient2.RoundId, outputAddress2, unblindedSignature2);
+ await bobClient1.PostOutputAsync(aliceClient1.RoundId, outputAddress1, unblindedSignature1, 0);
+ await bobClient2.PostOutputAsync(aliceClient2.RoundId, outputAddress2, unblindedSignature2, 0);
}
roundState = await satoshiClient.GetRoundStateAsync(aliceClient1.RoundId);
@@ -2201,7 +2201,7 @@ namespace WalletWasabi.Tests
InputProofModel inputProof = new InputProofModel { Input = input.ToTxoRef(), Proof = inputKey.SignCompact(blinded) };
InputProofModel[] inputsProofs = new InputProofModel[] { inputProof };
registerRequests.Add((changeOutputAddress, blinded, inputsProofs));
- await AliceClient.CreateNewAsync(network, changeOutputAddress, blinded, inputsProofs, baseUri);
+ await AliceClient.CreateNewAsync(network, changeOutputAddress, blinded, Enumerable.Empty<uint256>(), inputsProofs, baseUri);
}
await WaitForTimeoutAsync(baseUri);
@@ -2213,7 +2213,7 @@ namespace WalletWasabi.Tests
foreach (var registerRequest in registerRequests)
{
- await AliceClient.CreateNewAsync(network, registerRequest.changeOutputAddress, registerRequest.blindedData, registerRequest.inputsProofs, baseUri);
+ await AliceClient.CreateNewAsync(network, registerRequest.changeOutputAddress, registerRequest.blindedData, Enumerable.Empty<uint256>(), registerRequest.inputsProofs, baseUri);
}
await WaitForTimeoutAsync(baseUri);
@@ -2314,7 +2314,7 @@ namespace WalletWasabi.Tests
foreach (var user in inputRegistrationUsers)
{
- aliceClients.Add(AliceClient.CreateNewAsync(network, user.changeOutputAddress, user.blinded, user.inputProofModels, baseUri));
+ aliceClients.Add(AliceClient.CreateNewAsync(network, user.changeOutputAddress, user.blinded, Enumerable.Empty<uint256>(), user.inputProofModels, baseUri));
}
long roundId = 0;
@@ -2379,7 +2379,7 @@ namespace WalletWasabi.Tests
aliceClients.Clear();
foreach (var user in inputRegistrationUsers)
{
- aliceClients.Add(AliceClient.CreateNewAsync(network, user.changeOutputAddress, user.blinded, user.inputProofModels, baseUri));
+ aliceClients.Add(AliceClient.CreateNewAsync(network, user.changeOutputAddress, user.blinded, Enumerable.Empty<uint256>(), user.inputProofModels, baseUri));
}
roundId = 0;
@@ -2521,7 +2521,7 @@ namespace WalletWasabi.Tests
foreach (var user in inputRegistrationUsers)
{
- aliceClients.Add(AliceClient.CreateNewAsync(network, user.changeOutputAddress, user.blinded, user.inputProofModels, baseUri));
+ aliceClients.Add(AliceClient.CreateNewAsync(network, user.changeOutputAddress, user.blinded, Enumerable.Empty<uint256>(), user.inputProofModels, baseUri));
}
long roundId = 0;
@@ -2572,7 +2572,7 @@ namespace WalletWasabi.Tests
foreach (var user in users)
{
var bobClient = new BobClient(baseUri);
- outputRequests.Add((bobClient, bobClient.PostOutputAsync(roundId, user.activeOutputAddress, user.unblindedSignature)));
+ outputRequests.Add((bobClient, bobClient.PostOutputAsync(roundId, user.activeOutputAddress, user.unblindedSignature, 0)));
}
foreach (var request in outputRequests)

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/CcjClientRound.cs
WalletWasabi/Models/ChaumianCoinJoin/CcjClientRound.cs
@@ -17,8 +17,10 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
public BitcoinAddress ChangeOutputAddress { get; set; }
public BitcoinAddress ActiveOutputAddress { get; set; }
+ public BitcoinAddress[] AdditionalActiveOutputAddresses { get; set; }
public BlindSignature UnblindedSignature { get; set; }
+ public BlindSignature[] AdditionalUnblindedSignatures { get; set; }
/// <summary>
/// Connection has been confirmed in ConnectionConfirmation Phase. Used to avoid duplicate Connection Confirmation requests.
@@ -40,7 +42,9 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
CoinsRegistered.Clear();
ChangeOutputAddress = null;
ActiveOutputAddress = null;
+ AdditionalActiveOutputAddresses = null;
UnblindedSignature = null;
+ AdditionalUnblindedSignatures = null;
ConnectionFinalConfirmed = false;
AliceClient?.Dispose();
AliceClient = null;

MODIFY
WalletWasabi/Services/CcjClient.cs
WalletWasabi/Services/CcjClient.cs
@@ -303,7 +303,9 @@ namespace WalletWasabi.Services
private Dictionary<int, WitScript> SignCoinJoin(CcjClientRound ongoingRound, Transaction unsignedCoinJoin)
{
Money amountBack = unsignedCoinJoin.Outputs
- .Where(x => x.ScriptPubKey == ongoingRound.ActiveOutputAddress.ScriptPubKey || x.ScriptPubKey == ongoingRound.ChangeOutputAddress.ScriptPubKey)
+ .Where(x => x.ScriptPubKey == ongoingRound.ActiveOutputAddress.ScriptPubKey
+ || x.ScriptPubKey == ongoingRound.ChangeOutputAddress.ScriptPubKey
+ || ongoingRound.AdditionalActiveOutputAddresses.Select(y => y.ScriptPubKey).Contains(x.ScriptPubKey))
.Select(y => y.Value)
.Sum();
Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.Amount); // Start with input sum.
@@ -357,12 +359,23 @@ namespace WalletWasabi.Services
{
using (var bobClient = new BobClient(CcjHostUri, TorSocks5EndPoint))
{
- await bobClient.PostOutputAsync(ongoingRound.AliceClient.RoundId, ongoingRound.ActiveOutputAddress, ongoingRound.UnblindedSignature);
- ongoingRound.PostedOutput = true;
- Logger.LogInfo<AliceClient>($"Round ({ongoingRound.State.RoundId}) Bob Posted output.");
+ await bobClient.PostOutputAsync(ongoingRound.AliceClient.RoundId, ongoingRound.ActiveOutputAddress, ongoingRound.UnblindedSignature, 0);
+ }
+
+ for (int i = 0; i < ongoingRound.AdditionalUnblindedSignatures.Length; i++)
+ {
+ BlindSignature unblindedSignature = ongoingRound.AdditionalUnblindedSignatures[i];
+ BitcoinAddress activeOutputAddress = ongoingRound.AdditionalActiveOutputAddresses[i];
+ using (var bobClient = new BobClient(CcjHostUri, TorSocks5EndPoint))
+ {
+ await bobClient.PostOutputAsync(ongoingRound.AliceClient.RoundId, activeOutputAddress, unblindedSignature, i + 1);
}
}
+ ongoingRound.PostedOutput = true;
+ Logger.LogInfo<AliceClient>($"Round ({ongoingRound.State.RoundId}) Bob Posted outputs: {ongoingRound.AdditionalUnblindedSignatures.Length + 1}.");
+ }
+
private async Task TryConfirmConnectionAsync(CcjClientRound inputRegistrableRound)
{
try
@@ -397,7 +410,7 @@ namespace WalletWasabi.Services
if (registrableCoins.Any())
{
BitcoinAddress changeAddress = null;
- BitcoinAddress activeAddress = null;
+ var activeOutputAddresses = new List<BitcoinAddress>();
lock (CustomChangeAddressesLock)
{
if (CustomChangeAddresses.Count > 0)
@@ -408,14 +421,14 @@ namespace WalletWasabi.Services
}
lock (CustomActiveAddressesLock)
{
- if (CustomActiveAddresses.Count > 0)
+ foreach (BitcoinAddress customActiveAddress in CustomActiveAddresses.Take(10))
{
- activeAddress = CustomActiveAddresses.First();
- CustomActiveAddresses.RemoveFirst();
+ activeOutputAddresses.Add(customActiveAddress);
}
+ RemoveCustomActiveAddresses(activeOutputAddresses, noLock: true);
}
- if (changeAddress is null || activeAddress is null)
+ if (changeAddress is null || !activeOutputAddresses.Any())
{
IEnumerable<HdPubKey> allUnusedInternalKeys = KeyManager.GetKeys(keyState: null, isInternal: true).Where(x => x.KeyState != KeyState.Used);
@@ -449,7 +462,7 @@ namespace WalletWasabi.Services
AccessCache.AddOrReplace(changeKey, DateTimeOffset.UtcNow);
}
- if (activeAddress is null)
+ if (!activeOutputAddresses.Any())
{
string activeLabel = "ZeroLink Mixed Coin";
IEnumerable<HdPubKey> allActiveKeys = allUnusedInternalKeys.Where(x => x.Label == activeLabel);
@@ -469,25 +482,40 @@ namespace WalletWasabi.Services
}
activeKey.SetLabel(activeLabel);
activeKey.SetKeyState(KeyState.Locked);
- activeAddress = activeKey.GetP2wpkhAddress(Network);
+
+ //activeAddress = activeKey.GetP2wpkhAddress(Network);
+ activeOutputAddresses.Add(activeKey.GetP2wpkhAddress(Network));
}
else
{
activeKey = internalNotCachedLockedKeys.Where(x => changeAddress != x.GetP2wpkhAddress(Network)).RandomElement();
activeKey.SetLabel(activeLabel);
}
- activeAddress = activeKey.GetP2wpkhAddress(Network);
+ //activeAddress = activeKey.GetP2wpkhAddress(Network);
+ activeOutputAddresses.Add(activeKey.GetP2wpkhAddress(Network));
AccessCache.AddOrReplace(activeKey, DateTimeOffset.UtcNow);
}
}
KeyManager.ToFile();
- PubKey roundSignerPubKey = inputRegistrableRound.State.SignerPubKey;
+ var activeAddress = activeOutputAddresses.First();
+ var additionalActiveOutputAddresses = activeOutputAddresses.Skip(1);
+ PubKey signerPubKey = inputRegistrableRound.State.SignerPubKey;
+ PubKey[] additionalSignerPubKeys = inputRegistrableRound.State.AdditionalSignerPubKeys.Select(x => new PubKey(x)).ToArray();
PubKey roundRpubKey = inputRegistrableRound.State.RpubKey;
var outputScriptHash = new uint256(Hashes.SHA256(activeAddress.ScriptPubKey.ToBytes()));
var requester = new Requester();
- uint256 blindedOutputScriptHash = requester.BlindMessage(outputScriptHash, roundRpubKey, roundSignerPubKey);
+ uint256 blindedOutputScriptHash = requester.BlindMessage(outputScriptHash, roundRpubKey, signerPubKey);
+
+ var additionalBlindedOutputScriptHashes = new List<uint256>();
+ uint256[] array = additionalActiveOutputAddresses.Select(x => new uint256(Hashes.SHA256(x.ScriptPubKey.ToBytes()))).ToArray();
+ for (int i = 0; i < array.Length; i++)
+ {
+ uint256 osh = (uint256)array[i];
+ PubKey skp = additionalSignerPubKeys[i];
+ additionalBlindedOutputScriptHashes.Add(requester.BlindMessage(osh, roundRpubKey, skp));
+ }
var inputProofs = new List<InputProofModel>();
foreach ((uint256 txid, uint index) coinReference in registrableCoins)
@@ -506,7 +534,7 @@ namespace WalletWasabi.Services
AliceClient aliceClient = null;
try
{
- aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blindedOutputScriptHash, inputProofs, CcjHostUri, TorSocks5EndPoint);
+ aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blindedOutputScriptHash, additionalBlindedOutputScriptHashes, inputProofs, CcjHostUri, TorSocks5EndPoint);
}
catch (HttpRequestException ex) when (ex.Message.Contains("Input is banned", StringComparison.InvariantCultureIgnoreCase))
{
@@ -542,11 +570,21 @@ namespace WalletWasabi.Services
}
BlindSignature unblindedSignature = requester.UnblindSignature(aliceClient.BlindedOutputSignature);
+ if (!VerifySignature(outputScriptHash, unblindedSignature, signerPubKey))
+ {
+ throw new NotSupportedException("Coordinator did not sign the blinded output properly.");
+ }
- if (!VerifySignature(outputScriptHash, unblindedSignature, roundSignerPubKey))
+ var additionalUnblindedSignatures = new List<BlindSignature>();
+ foreach (var blindedOutputSignature in aliceClient.AdditionalBlindedOutputSignatures)
+ {
+ BlindSignature us = requester.UnblindSignature(blindedOutputSignature);
+ if (!VerifySignature(outputScriptHash, us, signerPubKey))
{
throw new NotSupportedException("Coordinator did not sign the blinded output properly.");
}
+ additionalUnblindedSignatures.Add(us);
+ }
CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId);
if (roundRegistered is null)
@@ -565,8 +603,10 @@ namespace WalletWasabi.Services
State.RemoveCoinFromWaitingList(coin);
}
roundRegistered.ActiveOutputAddress = activeAddress;
+ roundRegistered.AdditionalActiveOutputAddresses = additionalActiveOutputAddresses.Take(additionalUnblindedSignatures.Count()).ToArray();
roundRegistered.ChangeOutputAddress = changeAddress;
roundRegistered.UnblindedSignature = unblindedSignature;
+ roundRegistered.AdditionalUnblindedSignatures = additionalUnblindedSignatures.ToArray();
roundRegistered.AliceClient = aliceClient;
}
}
@@ -670,6 +710,21 @@ namespace WalletWasabi.Services
}
}
+ public void RemoveCustomActiveAddresses(IEnumerable<BitcoinAddress> addresses, bool noLock = false)
+ {
+ if (noLock)
+ {
+ CustomActiveAddresses.RemoveAll(x => addresses.Contains(x));
+ }
+ else
+ {
+ lock (CustomActiveAddressesLock)
+ {
+ CustomActiveAddresses.RemoveAll(x => addresses.Contains(x));
+ }
+ }
+ }
+
#endregion CustomAddressesLists
internal string OnePiece { get; private set; } = null;

MODIFY
WalletWasabi/WebClients/Wasabi/ChaumianCoinJoin/AliceClient.cs
WalletWasabi/WebClients/Wasabi/ChaumianCoinJoin/AliceClient.cs
@@ -24,6 +24,7 @@ namespace WalletWasabi.WebClients.Wasabi.ChaumianCoinJoin
public long RoundId { get; private set; }
public Guid UniqueId { get; private set; }
public BigInteger BlindedOutputSignature { get; private set; }
+ public BigInteger[] AdditionalBlindedOutputSignatures { get; private set; }
public Network Network { get; }
/// <inheritdoc/>
@@ -49,6 +50,7 @@ namespace WalletWasabi.WebClients.Wasabi.ChaumianCoinJoin
client.RoundId = inputsResponse.RoundId;
client.UniqueId = inputsResponse.UniqueId;
client.BlindedOutputSignature = inputsResponse.BlindedOutputSignature;
+ client.AdditionalBlindedOutputSignatures = inputsResponse.AdditionalBlindedOutputSignatures.Select(x => new BigInteger(x)).ToArray();
Logger.LogInfo<AliceClient>($"Round ({client.RoundId}), Alice ({client.UniqueId}): Registered {request.Inputs.Count()} inputs.");
return client;
@@ -61,12 +63,13 @@ namespace WalletWasabi.WebClients.Wasabi.ChaumianCoinJoin
}
}
- public static async Task<AliceClient> CreateNewAsync(Network network, BitcoinAddress changeOutput, uint256 blindedOutputScriptHash, IEnumerable<InputProofModel> inputs, Uri baseUri, IPEndPoint torSocks5EndPoint = null)
+ public static async Task<AliceClient> CreateNewAsync(Network network, BitcoinAddress changeOutput, uint256 blindedOutputScriptHash, IEnumerable<uint256> additionalBlindedOutputScriptHashes, IEnumerable<InputProofModel> inputs, Uri baseUri, IPEndPoint torSocks5EndPoint = null)
{
var request = new InputsRequest
{
BlindedOutputScript = blindedOutputScriptHash,
ChangeOutputAddress = changeOutput,
+ AdditionalBlindedOutputScripts = additionalBlindedOutputScriptHashes.Select(x => x.ToString()),
Inputs = inputs
};
return await CreateNewAsync(network, request, baseUri, torSocks5EndPoint);

MODIFY
WalletWasabi/WebClients/Wasabi/ChaumianCoinJoin/BobClient.cs
WalletWasabi/WebClients/Wasabi/ChaumianCoinJoin/BobClient.cs
@@ -16,9 +16,9 @@ namespace WalletWasabi.WebClients.Wasabi.ChaumianCoinJoin
{
}
- public async Task PostOutputAsync(long roundId, BitcoinAddress activeOutputAddress, BlindSignature unblindedSignature)
+ public async Task PostOutputAsync(long roundId, BitcoinAddress activeOutputAddress, BlindSignature unblindedSignature, int level)
{
- var request = new OutputRequest { OutputAddress = activeOutputAddress, UnblindedSignature = unblindedSignature };
+ var request = new OutputRequest { OutputAddress = activeOutputAddress, UnblindedSignature = unblindedSignature, Level = level };
using (var response = await TorClient.SendAsync(HttpMethod.Post, $"/api/v{Helpers.Constants.BackendMajorVersion}/btc/chaumiancoinjoin/output?roundId={roundId}", request.ToHttpStringContent()))
{
if (response.StatusCode != HttpStatusCode.NoContent)

Implement client side