Exemple #1
0
        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);
            }
        }