Separate address selection logic from registration logic
MODIFY
WalletWasabi/Services/CcjClient.cs
WalletWasabi/Services/CcjClient.cs
@@ -454,9 +454,120 @@ namespace WalletWasabi.Services
// If there are no suitable coins to register return.
if (!registrableCoins.Any()) return;
- BitcoinAddress changeAddress = null;
- var activeOutputAddresses = new List<BitcoinAddress>();
+ (BitcoinAddress change, IEnumerable<BitcoinAddress> active) outputAddresses = GetOutputAddressesToRegister(inputRegistrableRound, registrableCoins);
+
+ SchnorrPubKey[] schnorrPubKeys = inputRegistrableRound.State.SchnorrPubKeys.ToArray();
+ List<Requester> requesters = new List<Requester>();
+ var outputScriptHashes = new List<uint256>();
+ var blindedOutputScriptHashes = new List<uint256>();
+
+ var registeredAddresses = new List<BitcoinAddress>();
+ for (int i = 0; i < schnorrPubKeys.Length; i++)
+ {
+ if (outputAddresses.active.Count() <= i) break;
+ BitcoinAddress address = outputAddresses.active.ElementAt(i);
+
+ SchnorrPubKey schnorrPubKey = schnorrPubKeys[i];
+ var outputScriptHash = new uint256(Hashes.SHA256(address.ScriptPubKey.ToBytes()));
+ var requester = new Requester();
+ uint256 blindedOutputScriptHash = requester.BlindMessage(outputScriptHash, schnorrPubKey);
+ outputScriptHashes.Add(outputScriptHash);
+ requesters.Add(requester);
+ blindedOutputScriptHashes.Add(blindedOutputScriptHash);
+ registeredAddresses.Add(address);
+ }
+
+ byte[] blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputScriptHashes.Select(x => x.ToBytes()));
+ uint256 blindedOutputScriptsHash = new uint256(Hashes.SHA256(blindedOutputScriptHashesByte));
+
+ var inputProofs = new List<InputProofModel>();
+ foreach (TxoRef coinReference in registrableCoins)
+ {
+ SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
+ if (coin is null) throw new NotSupportedException("This is impossible.");
+ coin.Secret = coin.Secret ?? KeyManager.GetSecrets(SaltSoup(), coin.ScriptPubKey).Single();
+ var inputProof = new InputProofModel
+ {
+ Input = coin.GetTxoRef(),
+ Proof = coin.Secret.PrivateKey.SignCompact(blindedOutputScriptsHash)
+ };
+ inputProofs.Add(inputProof);
+ }
+
+ AliceClient aliceClient = null;
+ try
+ {
+ aliceClient = await AliceClient.CreateNewAsync(Network, outputAddresses.change, blindedOutputScriptHashes, 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);
+ TxoRef coinReference = new TxoRef(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]);
+
+ await DequeueCoinsFromMixNoLockAsync(coinReference);
+ return;
+ }
+ catch (HttpRequestException ex) when (ex.Message.Contains("Provided input is not unspent", StringComparison.InvariantCultureIgnoreCase))
+ {
+ string[] parts = ex.Message.Split(new[] { "Provided input is not unspent: " }, StringSplitOptions.RemoveEmptyEntries);
+ string spentInputString = parts[1].TrimEnd('.');
+ string[] bannedInputStringParts = spentInputString.Split(':', StringSplitOptions.RemoveEmptyEntries);
+ TxoRef coinReference = new TxoRef(new uint256(bannedInputStringParts[1]), uint.Parse(bannedInputStringParts[0]));
+ SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
+ if (coin is null) throw new NotSupportedException("This is impossible.");
+ coin.SpentAccordingToBackend = true;
+
+ Logger.LogWarning<CcjClient>(ex.Message.Split('\n')[1]);
+
+ await DequeueCoinsFromMixNoLockAsync(coinReference);
+ return;
+ }
+
+ aliceClient.RegisteredAddresses = registeredAddresses.ToArray();
+ aliceClient.SchnorrPubKeys = schnorrPubKeys;
+ aliceClient.Requesters = requesters.ToArray();
+ aliceClient.OutputScriptHashes = outputScriptHashes.ToArray();
+
+ var coinsRegistered = new List<SmartCoin>();
+ foreach (TxoRef coinReference in registrableCoins)
+ {
+ var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
+ if (coin is null) throw new NotSupportedException("This is impossible.");
+ coinsRegistered.Add(coin);
+ State.RemoveCoinFromWaitingList(coin);
+ }
+
+ var registration = new ClientRoundRegistration(aliceClient, coinsRegistered, outputAddresses.change);
+
+ CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId);
+ if (roundRegistered is null)
+ {
+ // If our SatoshiClient doesn't yet know about the round, because of delay, then delay the round registration.
+ DelayedRoundRegistration?.Dispose();
+ DelayedRoundRegistration = registration;
+ }
+
+ roundRegistered.Registration = registration;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError<CcjClient>(ex);
+ }
+ }
+ private (BitcoinAddress change, IEnumerable<BitcoinAddress> active) GetOutputAddressesToRegister(CcjClientRound inputRegistrableRound, List<TxoRef> registrableCoins)
+ {
+ BitcoinAddress changeOutputAddress = null;
+ var activeOutputAddresses = new List<BitcoinAddress>();
string changeLabel = "ZeroLink Change";
string activeLabel = "ZeroLink Mixed Coin";
@@ -483,7 +594,7 @@ namespace WalletWasabi.Services
changeKey = internalNotCachedLockedKeys.RandomElement();
changeKey.SetLabel(changeLabel);
}
- changeAddress = changeKey.GetP2wpkhAddress(Network);
+ changeOutputAddress = changeKey.GetP2wpkhAddress(Network);
AccessCache.AddOrReplace(changeKey, DateTimeOffset.UtcNow);
IEnumerable<HdPubKey> allActiveKeys = KeyManager.GetKeys(x => x.KeyState != KeyState.Used && x.Label == activeLabel);
@@ -531,7 +642,7 @@ namespace WalletWasabi.Services
// If there is no such a key, then use the oldest, but make sure it's not the same as the change.
if (activeKeys.Count < maximumMixingLevelCount)
{
- foreach (var ac in AccessCache.Where(x => allActiveKeys.Contains(x.Key) && changeAddress != x.Key.GetP2wpkhAddress(Network)).OrderBy(x => x.Value).Select(x => x.Key))
+ foreach (var ac in AccessCache.Where(x => allActiveKeys.Contains(x.Key) && changeOutputAddress != x.Key.GetP2wpkhAddress(Network)).OrderBy(x => x.Value).Select(x => x.Key))
{
if (activeKeys.Count >= maximumMixingLevelCount)
{
@@ -543,7 +654,7 @@ namespace WalletWasabi.Services
}
else
{
- foreach (var ac in internalNotCachedLockedKeys.Where(x => changeAddress != x.GetP2wpkhAddress(Network)))
+ foreach (var ac in internalNotCachedLockedKeys.Where(x => changeOutputAddress != x.GetP2wpkhAddress(Network)))
{
if (activeKeys.Count >= maximumMixingLevelCount)
{
@@ -563,112 +674,7 @@ namespace WalletWasabi.Services
KeyManager.ToFile();
- SchnorrPubKey[] schnorrPubKeys = inputRegistrableRound.State.SchnorrPubKeys.ToArray();
- List<Requester> requesters = new List<Requester>();
- var outputScriptHashes = new List<uint256>();
- var blindedOutputScriptHashes = new List<uint256>();
-
- var registeredAddresses = new List<BitcoinAddress>();
- for (int i = 0; i < schnorrPubKeys.Length; i++)
- {
- if (activeOutputAddresses.Count <= i) break;
- BitcoinAddress address = activeOutputAddresses.ElementAt(i);
-
- SchnorrPubKey schnorrPubKey = schnorrPubKeys[i];
- var outputScriptHash = new uint256(Hashes.SHA256(address.ScriptPubKey.ToBytes()));
- var requester = new Requester();
- uint256 blindedOutputScriptHash = requester.BlindMessage(outputScriptHash, schnorrPubKey);
- outputScriptHashes.Add(outputScriptHash);
- requesters.Add(requester);
- blindedOutputScriptHashes.Add(blindedOutputScriptHash);
- registeredAddresses.Add(address);
- }
-
- byte[] blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputScriptHashes.Select(x => x.ToBytes()));
- uint256 blindedOutputScriptsHash = new uint256(Hashes.SHA256(blindedOutputScriptHashesByte));
-
- var inputProofs = new List<InputProofModel>();
- foreach (TxoRef coinReference in registrableCoins)
- {
- SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
- if (coin is null) throw new NotSupportedException("This is impossible.");
- coin.Secret = coin.Secret ?? KeyManager.GetSecrets(SaltSoup(), coin.ScriptPubKey).Single();
- var inputProof = new InputProofModel
- {
- Input = coin.GetTxoRef(),
- Proof = coin.Secret.PrivateKey.SignCompact(blindedOutputScriptsHash)
- };
- inputProofs.Add(inputProof);
- }
-
- AliceClient aliceClient = null;
- try
- {
- aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blindedOutputScriptHashes, 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);
- TxoRef coinReference = new TxoRef(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]);
-
- await DequeueCoinsFromMixNoLockAsync(coinReference);
- return;
- }
- catch (HttpRequestException ex) when (ex.Message.Contains("Provided input is not unspent", StringComparison.InvariantCultureIgnoreCase))
- {
- string[] parts = ex.Message.Split(new[] { "Provided input is not unspent: " }, StringSplitOptions.RemoveEmptyEntries);
- string spentInputString = parts[1].TrimEnd('.');
- string[] bannedInputStringParts = spentInputString.Split(':', StringSplitOptions.RemoveEmptyEntries);
- TxoRef coinReference = new TxoRef(new uint256(bannedInputStringParts[1]), uint.Parse(bannedInputStringParts[0]));
- SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
- if (coin is null) throw new NotSupportedException("This is impossible.");
- coin.SpentAccordingToBackend = true;
-
- Logger.LogWarning<CcjClient>(ex.Message.Split('\n')[1]);
-
- await DequeueCoinsFromMixNoLockAsync(coinReference);
- return;
- }
-
- aliceClient.RegisteredAddresses = registeredAddresses.ToArray();
- aliceClient.SchnorrPubKeys = schnorrPubKeys;
- aliceClient.Requesters = requesters.ToArray();
- aliceClient.OutputScriptHashes = outputScriptHashes.ToArray();
-
- var coinsRegistered = new List<SmartCoin>();
- foreach (TxoRef coinReference in registrableCoins)
- {
- var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
- if (coin is null) throw new NotSupportedException("This is impossible.");
- coinsRegistered.Add(coin);
- State.RemoveCoinFromWaitingList(coin);
- }
-
- var registration = new ClientRoundRegistration(aliceClient, coinsRegistered, changeAddress);
-
- CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId);
- if (roundRegistered is null)
- {
- // If our SatoshiClient doesn't yet know about the round, because of delay, then delay the round registration.
- DelayedRoundRegistration?.Dispose();
- DelayedRoundRegistration = registration;
- }
-
- roundRegistered.Registration = registration;
- }
- catch (Exception ex)
- {
- Logger.LogError<CcjClient>(ex);
- }
+ return (changeOutputAddress, activeOutputAddresses);
}
public async Task QueueCoinsToMixAsync(params SmartCoin[] coins)

Separate address selection logic from registration logic