private async Task TryRegisterCoinsAsync(CcjClientRound inputRegistrableRound) { try { List <(uint256 txid, uint index)> registrableCoins = State.GetRegistrableCoins( inputRegistrableRound.State.MaximumInputCountPerPeer, inputRegistrableRound.State.Denomination, inputRegistrableRound.State.FeePerInputs, inputRegistrableRound.State.FeePerOutputs).ToList(); if (registrableCoins.Any()) { BitcoinAddress changeAddress = null; BitcoinAddress activeAddress = null; lock (CustomChangeAddressesLock) { if (CustomChangeAddresses.Count > 0) { changeAddress = CustomChangeAddresses.First(); CustomChangeAddresses.RemoveFirst(); } } lock (CustomActiveAddressesLock) { if (CustomActiveAddresses.Count > 0) { activeAddress = CustomActiveAddresses.First(); CustomActiveAddresses.RemoveFirst(); } } if (changeAddress is null || activeAddress is null) { IEnumerable <HdPubKey> allUnusedInternalKeys = KeyManager.GetKeys(keyState: null, isInternal: true).Where(x => x.KeyState != KeyState.Used); if (changeAddress is null) { string changeLabel = "ZeroLink Change"; IEnumerable <HdPubKey> allChangeKeys = allUnusedInternalKeys.Where(x => x.Label == changeLabel); HdPubKey changeKey = null; KeyManager.AssertLockedInternalKeysIndexed(14); IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys); if (allChangeKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any()) // Then don't generate new keys, because it'd bloat the wallet. { // Find the first one that we did not try to register in the current session. changeKey = allChangeKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x)); // If there is no such a key, then use the oldest. if (changeKey == default) { changeKey = AccessCache.Where(x => allChangeKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key; } changeKey.SetLabel(changeLabel); changeKey.SetKeyState(KeyState.Locked); } else { changeKey = internalNotCachedLockedKeys.RandomElement(); changeKey.SetLabel(changeLabel); } changeAddress = changeKey.GetP2wpkhAddress(Network); AccessCache.AddOrReplace(changeKey, DateTimeOffset.UtcNow); } if (activeAddress is null) { string activeLabel = "ZeroLink Mixed Coin"; IEnumerable <HdPubKey> allActiveKeys = allUnusedInternalKeys.Where(x => x.Label == activeLabel); HdPubKey activeKey = null; KeyManager.AssertLockedInternalKeysIndexed(14); IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys); if (allActiveKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any()) // Then don't generate new keys, because it'd bloat the wallet. { // Find the first one that we did not try to register in the current session. activeKey = allActiveKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x)); // If there is no such a key, then use the oldest. if (activeKey == default) { activeKey = AccessCache.Where(x => allActiveKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key; } activeKey.SetLabel(activeLabel); activeKey.SetKeyState(KeyState.Locked); activeAddress = activeKey.GetP2wpkhAddress(Network); } else { activeKey = internalNotCachedLockedKeys.RandomElement(); activeKey.SetLabel(activeLabel); } activeAddress = activeKey.GetP2wpkhAddress(Network); AccessCache.AddOrReplace(activeKey, DateTimeOffset.UtcNow); } } KeyManager.ToFile(); var blind = CoordinatorPubKey.Blind(activeAddress.ScriptPubKey.ToBytes()); var inputProofs = new List <InputProofModel>(); foreach ((uint256 txid, uint index)coinReference in registrableCoins) { SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } coin.Secret = coin.Secret ?? KeyManager.GetSecrets(OnePiece, coin.ScriptPubKey).Single(); var inputProof = new InputProofModel { Input = coin.GetTxoRef(), Proof = coin.Secret.PrivateKey.SignMessage(ByteHelpers.ToHex(blind.BlindedData)) }; inputProofs.Add(inputProof); } AliceClient aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blind.BlindedData, inputProofs, CcjHostUri, TorSocks5EndPoint); byte[] unblindedSignature = CoordinatorPubKey.UnblindSignature(aliceClient.BlindedOutputSignature, blind.BlindingFactor); if (!CoordinatorPubKey.Verify(unblindedSignature, activeAddress.ScriptPubKey.ToBytes())) { throw new NotSupportedException("Coordinator did not sign the blinded output properly."); } CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId); if (roundRegistered is null) { // If our SatoshiClient doesn't yet know about the round because of the dealy create it. // Make its state as it'd be the same as our assumed round was, except the roundId and registeredPeerCount, it'll be updated later. roundRegistered = new CcjClientRound(CcjRunningRoundState.CloneExcept(inputRegistrableRound.State, aliceClient.RoundId, registeredPeerCount: 1)); State.AddOrReplaceRound(roundRegistered); } foreach ((uint256 txid, uint index)coinReference in registrableCoins) { var coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } roundRegistered.CoinsRegistered.Add(coin); State.RemoveCoinFromWaitingList(coin); } roundRegistered.ActiveOutputAddress = activeAddress; roundRegistered.ChangeOutputAddress = changeAddress; roundRegistered.UnblindedSignature = unblindedSignature; roundRegistered.AliceClient = aliceClient; } } catch (Exception ex) { Logger.LogError <CcjClient>(ex); } }
private async Task TryRegisterCoinsAsync(CcjClientRound inputRegistrableRound) { try { // Select the most suitable coins to regiter. List <TxoRef> registrableCoins = State.GetRegistrableCoins( inputRegistrableRound.State.MaximumInputCountPerPeer, inputRegistrableRound.State.Denomination, inputRegistrableRound.State.FeePerInputs, inputRegistrableRound.State.FeePerOutputs).ToList(); // If there are no suitable coins to register return. if (!registrableCoins.Any()) { return; } (HdPubKey change, IEnumerable <HdPubKey> actives)outputAddresses = GetOutputsToRegister(inputRegistrableRound.State.Denomination, inputRegistrableRound.State.SchnorrPubKeys.Count(), registrableCoins); SchnorrPubKey[] schnorrPubKeys = inputRegistrableRound.State.SchnorrPubKeys.ToArray(); List <Requester> requesters = new List <Requester>(); var blindedOutputScriptHashes = new List <uint256>(); var registeredAddresses = new List <BitcoinAddress>(); for (int i = 0; i < schnorrPubKeys.Length; i++) { if (outputAddresses.actives.Count() <= i) { break; } BitcoinAddress address = outputAddresses.actives.Select(x => x.GetP2wpkhAddress(Network)).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); 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(inputRegistrableRound.RoundId, registeredAddresses, schnorrPubKeys, requesters, Network, outputAddresses.change.GetP2wpkhAddress(Network), blindedOutputScriptHashes, inputProofs, CcjHostUriAction, 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, "Failed to register the coin with the coordinator."); aliceClient?.Dispose(); 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, "Failed to register the coin with the coordinator. The coin is already spent."); aliceClient?.Dispose(); return; } catch (HttpRequestException ex) when(ex.Message.Contains("No such running round in InputRegistration.", StringComparison.InvariantCultureIgnoreCase)) { Logger.LogInfo <CcjClient>("Client tried to register a round that is not in InputRegistration anymore. Trying again later."); aliceClient?.Dispose(); return; } catch (HttpRequestException ex) when(ex.Message.Contains("too-long-mempool-chain", StringComparison.InvariantCultureIgnoreCase)) { Logger.LogInfo <CcjClient>("Coordinator failed because too much unconfirmed parent transactions. Trying again later."); aliceClient?.Dispose(); return; } 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.GetP2wpkhAddress(Network)); CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId); if (roundRegistered is null) { // If our SatoshiClient does not 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); } }