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);
            }
        }
Exemple #2
0
    public async Task BanningTestsAsync()
    {
        (_, IRPCClient rpc, Network network, Coordinator coordinator, _, _, _) = await Common.InitializeTestEnvironmentAsync(RegTestFixture, 1);

        Money   denomination                  = Money.Coins(0.1m);
        decimal coordinatorFeePercent         = 0.1m;
        int     anonymitySet                  = 3;
        int     connectionConfirmationTimeout = 120;
        var     roundConfig = RegTestFixture.CreateRoundConfig(denomination, 140, 0.7, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 1, 1, 1, 24, true, 11);

        coordinator.RoundConfig.UpdateOrDefault(roundConfig, toFile: true);
        coordinator.AbortAllRoundsInInputRegistration("");

        await rpc.GenerateAsync(3);         // So to make sure we have enough money.

        Uri baseUri        = new(RegTestFixture.BackendEndPoint);
        var fundingTxCount = 0;
        List <(Requester requester, BlindedOutputWithNonceIndex blinded, BitcoinAddress activeOutputAddress, BitcoinAddress changeOutputAddress, IEnumerable <InputProofModel> inputProofModels, List <(Key key, BitcoinAddress address, uint256 txHash, Transaction tx, OutPoint input)> userInputData)> inputRegistrationUsers = new();
        CoordinatorRound?round = null;

        for (int i = 0; i < roundConfig.AnonymitySet; i++)
        {
            List <(Key key, BitcoinAddress inputAddress, uint256 txHash, Transaction tx, OutPoint input)> userInputData = new();
            var activeOutputAddress = new Key().GetAddress(ScriptPubKeyType.Segwit, network);
            var changeOutputAddress = new Key().GetAddress(ScriptPubKeyType.Segwit, network);
            Assert.True(coordinator.TryGetCurrentInputRegisterableRound(out round));
            Requester requester = new();
            var       nonce     = round !.NonceProvider.GetNextNonce();

            BlindedOutputWithNonceIndex blinded = new(nonce.N, requester.BlindScript(round.MixingLevels.GetBaseLevel().SignerKey.PubKey, nonce.R, activeOutputAddress.ScriptPubKey));
            uint256 blindedOutputScriptsHash    = new(Hashes.SHA256(blinded.BlindedOutput.ToBytes()));

            List <InputProofModel> inputProofModels = new();
            int numberOfInputs    = CryptoHelpers.RandomInt(1, 6);
            var receiveSatoshiSum = 0;
            for (int j = 0; j < numberOfInputs; j++)
            {
                Key key            = new();
                var receiveSatoshi = CryptoHelpers.RandomInt(1000, 100000000);
                receiveSatoshiSum += receiveSatoshi;
                if (j == numberOfInputs - 1)
                {
                    receiveSatoshi = 100000000;
                }
                var     inputAddress = key.PubKey.GetAddress(ScriptPubKeyType.Segwit, network);
                uint256 txHash       = await rpc.SendToAddressAsync(inputAddress, Money.Satoshis(receiveSatoshi));

                fundingTxCount++;
                Assert.NotNull(txHash);
                Transaction transaction = await rpc.GetRawTransactionAsync(txHash);

                var coin = transaction.Outputs.GetCoins(inputAddress.ScriptPubKey).Single();

                OutPoint input      = coin.Outpoint;
                var      inputProof = new InputProofModel {
                    Input = input, Proof = key.SignCompact(blindedOutputScriptsHash)
                };
                inputProofModels.Add(inputProof);

                GetTxOutResponse?getTxOutResponse = await rpc.GetTxOutAsync(input.Hash, (int)input.N, includeMempool : true);

                // Check if inputs are unspent.
                Assert.NotNull(getTxOutResponse);

                userInputData.Add((key, inputAddress, txHash, transaction, input));
            }

            inputRegistrationUsers.Add((requester, blinded, activeOutputAddress, changeOutputAddress, inputProofModels, userInputData));
        }

        var mempool = await rpc.GetRawMempoolAsync();

        Assert.Equal(inputRegistrationUsers.SelectMany(x => x.userInputData).Count(), mempool.Length);

        while ((await rpc.GetRawMempoolAsync()).Length != 0)
        {
            await rpc.GenerateAsync(1);
        }

        List <Task <AliceClient4> > aliceClients = new();

        foreach (var user in inputRegistrationUsers)
        {
            aliceClients.Add(AliceClientBase.CreateNewAsync(round !.RoundId, new[] { user.activeOutputAddress }, new[] { round !.MixingLevels.GetBaseLevel().SignerKey.PubKey }, new[] { user.requester }, network, user.changeOutputAddress, new[] { user.blinded }, user.inputProofModels, BackendHttpClient));
        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);
            }
        }
Exemple #4
0
        public async Task BanningTestsAsync()
        {
            (_, IRPCClient rpc, Network network, Coordinator coordinator, _, _, _) = await Common.InitializeTestEnvironmentAsync(RegTestFixture, 1);

            Money   denomination                  = Money.Coins(0.1m);
            decimal coordinatorFeePercent         = 0.1m;
            int     anonymitySet                  = 3;
            int     connectionConfirmationTimeout = 120;
            var     roundConfig = RegTestFixture.CreateRoundConfig(denomination, 140, 0.7, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 1, 1, 1, 24, true, 11);

            coordinator.RoundConfig.UpdateOrDefault(roundConfig, toFile: true);
            coordinator.AbortAllRoundsInInputRegistration("");

            await rpc.GenerateAsync(3);             // So to make sure we have enough money.

            Uri baseUri                = new Uri(RegTestFixture.BackendEndPoint);
            var fundingTxCount         = 0;
            var inputRegistrationUsers = new List <(Requester requester, BlindedOutputWithNonceIndex blinded, BitcoinAddress activeOutputAddress, BitcoinAddress changeOutputAddress, IEnumerable <InputProofModel> inputProofModels, List <(Key key, BitcoinWitPubKeyAddress address, uint256 txHash, Transaction tx, OutPoint input)> userInputData)>();
            CoordinatorRound round     = null;

            for (int i = 0; i < roundConfig.AnonymitySet; i++)
            {
                var userInputData       = new List <(Key key, BitcoinWitPubKeyAddress inputAddress, uint256 txHash, Transaction tx, OutPoint input)>();
                var activeOutputAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Segwit, network);
                var changeOutputAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Segwit, network);
                round = coordinator.GetCurrentInputRegisterableRoundOrDefault();
                Requester requester = new Requester();
                var       nonce     = round.NonceProvider.GetNextNonce();

                var     blinded = new BlindedOutputWithNonceIndex(nonce.N, requester.BlindScript(round.MixingLevels.GetBaseLevel().SignerKey.PubKey, nonce.R, activeOutputAddress.ScriptPubKey));
                uint256 blindedOutputScriptsHash = new uint256(Hashes.SHA256(blinded.BlindedOutput.ToBytes()));

                var inputProofModels  = new List <InputProofModel>();
                int numberOfInputs    = CryptoHelpers.RandomInt(1, 6);
                var receiveSatoshiSum = 0;
                for (int j = 0; j < numberOfInputs; j++)
                {
                    var key            = new Key();
                    var receiveSatoshi = CryptoHelpers.RandomInt(1000, 100000000);
                    receiveSatoshiSum += receiveSatoshi;
                    if (j == numberOfInputs - 1)
                    {
                        receiveSatoshi = 100000000;
                    }
                    BitcoinWitPubKeyAddress inputAddress = key.PubKey.GetSegwitAddress(network);
                    uint256 txHash = await rpc.SendToAddressAsync(inputAddress, Money.Satoshis(receiveSatoshi));

                    fundingTxCount++;
                    Assert.NotNull(txHash);
                    Transaction transaction = await rpc.GetRawTransactionAsync(txHash);

                    var coin = transaction.Outputs.GetCoins(inputAddress.ScriptPubKey).Single();

                    OutPoint input      = coin.Outpoint;
                    var      inputProof = new InputProofModel {
                        Input = input, Proof = key.SignCompact(blindedOutputScriptsHash)
                    };
                    inputProofModels.Add(inputProof);

                    GetTxOutResponse getTxOutResponse = await rpc.GetTxOutAsync(input.Hash, (int)input.N, includeMempool : true);

                    // Check if inputs are unspent.
                    Assert.NotNull(getTxOutResponse);

                    userInputData.Add((key, inputAddress, txHash, transaction, input));
                }

                inputRegistrationUsers.Add((requester, blinded, activeOutputAddress, changeOutputAddress, inputProofModels, userInputData));
            }

            var mempool = await rpc.GetRawMempoolAsync();

            Assert.Equal(inputRegistrationUsers.SelectMany(x => x.userInputData).Count(), mempool.Length);

            while ((await rpc.GetRawMempoolAsync()).Length != 0)
            {
                await rpc.GenerateAsync(1);
            }

            var aliceClients = new List <Task <AliceClient4> >();

            foreach (var user in inputRegistrationUsers)
            {
                aliceClients.Add(AliceClientBase.CreateNewAsync(round.RoundId, new[] { user.activeOutputAddress }, new[] { round.MixingLevels.GetBaseLevel().SignerKey.PubKey }, new[] { user.requester }, network, user.changeOutputAddress, new[] { user.blinded }, user.inputProofModels, BackendHttpClient));
            }

            long roundId = 0;
            var  users   = new List <(Requester requester, BlindedOutputWithNonceIndex blinded, BitcoinAddress activeOutputAddress, BitcoinAddress changeOutputAddress, IEnumerable <InputProofModel> inputProofModels, List <(Key key, BitcoinWitPubKeyAddress address, uint256 txHash, Transaction tx, OutPoint input)> userInputData, AliceClient4 aliceClient, UnblindedSignature unblindedSignature)>();

            for (int i = 0; i < inputRegistrationUsers.Count; i++)
            {
                var user    = inputRegistrationUsers[i];
                var request = aliceClients[i];

                var aliceClient = await request;

                if (roundId == 0)
                {
                    roundId = aliceClient.RoundId;
                }
                else
                {
                    Assert.Equal(roundId, aliceClient.RoundId);
                }

                // Because it's valuetuple.
                users.Add((user.requester, user.blinded, user.activeOutputAddress, user.changeOutputAddress, user.inputProofModels, user.userInputData, aliceClient, null));
            }

            Assert.Equal(users.Count, roundConfig.AnonymitySet);

            var confirmationRequests = new List <Task <(RoundPhase currentPhase, IEnumerable <ActiveOutput>)> >();

            foreach (var user in users)
            {
                confirmationRequests.Add(user.aliceClient.PostConfirmationAsync());
            }

            RoundPhase roundPhase = RoundPhase.InputRegistration;
            int        k          = 0;

            foreach (var request in confirmationRequests)
            {
                var resp = await request;
                if (roundPhase == RoundPhase.InputRegistration)
                {
                    roundPhase = resp.currentPhase;
                }
                else
                {
                    Assert.Equal(roundPhase, resp.currentPhase);
                }

                var user = users.ElementAt(k);
                user.unblindedSignature = resp.Item2.First().Signature;
            }

            {
                var times = 0;
                while (!(await SatoshiClient.GetAllRoundStatesAsync()).All(x => x.Phase == RoundPhase.InputRegistration))
                {
                    await Task.Delay(100);

                    if (times > 50)                     // 5 sec, 3 should be enough
                    {
                        throw new TimeoutException("Not all rounds were in InputRegistration.");
                    }
                    times++;
                }
            }

            int bannedCount = coordinator.UtxoReferee.CountBanned(false);

            Assert.Equal(0, bannedCount);

            aliceClients.Clear();
            round = coordinator.GetCurrentInputRegisterableRoundOrDefault();
            foreach (var user in inputRegistrationUsers)
            {
                aliceClients.Add(AliceClientBase.CreateNewAsync(round.RoundId, new[] { user.activeOutputAddress }, new[] { round.MixingLevels.GetBaseLevel().SignerKey.PubKey }, new[] { user.requester }, network, user.changeOutputAddress, new[] { user.blinded }, user.inputProofModels, BackendHttpClient));
            }

            roundId = 0;
            users   = new List <(Requester requester, BlindedOutputWithNonceIndex blinded, BitcoinAddress activeOutputAddress, BitcoinAddress changeOutputAddress, IEnumerable <InputProofModel> inputProofModels, List <(Key key, BitcoinWitPubKeyAddress address, uint256 txHash, Transaction tx, OutPoint input)> userInputData, AliceClient4 aliceClient, UnblindedSignature unblindedSignature)>();
            for (int i = 0; i < inputRegistrationUsers.Count; i++)
            {
                var user    = inputRegistrationUsers[i];
                var request = aliceClients[i];

                var aliceClient = await request;
                if (roundId == 0)
                {
                    roundId = aliceClient.RoundId;
                }
                else
                {
                    Assert.Equal(roundId, aliceClient.RoundId);
                }

                // Because it's valuetuple.
                users.Add((user.requester, user.blinded, user.activeOutputAddress, user.changeOutputAddress, user.inputProofModels, user.userInputData, aliceClient, null));
            }

            Assert.Equal(users.Count, roundConfig.AnonymitySet);

            confirmationRequests = new List <Task <(RoundPhase currentPhase, IEnumerable <ActiveOutput>)> >();

            foreach (var user in users)
            {
                confirmationRequests.Add(user.aliceClient.PostConfirmationAsync());
            }

            {
                var times = 0;
                while (!(await SatoshiClient.GetAllRoundStatesAsync()).All(x => x.Phase == RoundPhase.InputRegistration))
                {
                    await Task.Delay(100);

                    if (times > 50)                     // 5 sec, 3 should be enough
                    {
                        throw new TimeoutException("Not all rounds were in InputRegistration.");
                    }
                    times++;
                }
            }

            bannedCount = coordinator.UtxoReferee.CountBanned(false);
            Assert.True(bannedCount >= roundConfig.AnonymitySet);

            foreach (var aliceClient in aliceClients)
            {
                aliceClient?.Dispose();
            }
        }
Exemple #5
0
        public async Task NotingTestsAsync()
        {
            (_, IRPCClient rpc, Network network, Coordinator coordinator, _, _, _) = await Common.InitializeTestEnvironmentAsync(RegTestFixture, 1);

            Money   denomination                  = Money.Coins(1m);
            decimal coordinatorFeePercent         = 0.1m;
            int     anonymitySet                  = 2;
            int     connectionConfirmationTimeout = 1;
            bool    doesNoteBeforeBan             = true;
            CoordinatorRoundConfig roundConfig    = RegTestFixture.CreateRoundConfig(denomination, 140, 0.7, coordinatorFeePercent, anonymitySet, 240, connectionConfirmationTimeout, 1, 1, 1, 24, doesNoteBeforeBan, 11);

            coordinator.RoundConfig.UpdateOrDefault(roundConfig, toFile: true);
            coordinator.AbortAllRoundsInInputRegistration("");

            Uri baseUri = new Uri(RegTestFixture.BackendEndPoint);

            var              registerRequests  = new List <(BitcoinWitPubKeyAddress changeOutputAddress, BlindedOutputWithNonceIndex blindedData, InputProofModel[] inputsProofs)>();
            AliceClient4     aliceClientBackup = null;
            CoordinatorRound round             = coordinator.GetCurrentInputRegisterableRoundOrDefault();

            for (int i = 0; i < roundConfig.AnonymitySet; i++)
            {
                BitcoinWitPubKeyAddress activeOutputAddress = new Key().PubKey.GetSegwitAddress(network);
                BitcoinWitPubKeyAddress changeOutputAddress = new Key().PubKey.GetSegwitAddress(network);
                Key inputKey = new Key();
                BitcoinWitPubKeyAddress inputAddress = inputKey.PubKey.GetSegwitAddress(network);

                var requester = new Requester();
                var nonce     = round.NonceProvider.GetNextNonce();

                var     blinded = new BlindedOutputWithNonceIndex(nonce.N, requester.BlindScript(round.MixingLevels.GetBaseLevel().SignerKey.PubKey, nonce.R, activeOutputAddress.ScriptPubKey));
                uint256 blindedOutputScriptsHash = new uint256(Hashes.SHA256(blinded.BlindedOutput.ToBytes()));

                uint256 txHash = await rpc.SendToAddressAsync(inputAddress, Money.Coins(2));

                await rpc.GenerateAsync(1);

                Transaction transaction = await rpc.GetRawTransactionAsync(txHash);

                Coin     coin  = transaction.Outputs.GetCoins(inputAddress.ScriptPubKey).Single();
                OutPoint input = coin.Outpoint;

                InputProofModel inputProof = new InputProofModel {
                    Input = input, Proof = inputKey.SignCompact(blindedOutputScriptsHash)
                };
                InputProofModel[] inputsProofs = new InputProofModel[] { inputProof };
                registerRequests.Add((changeOutputAddress, blinded, inputsProofs));
                aliceClientBackup = await AliceClientBase.CreateNewAsync(round.RoundId, new[] { activeOutputAddress }, new[] { round.MixingLevels.GetBaseLevel().SignerKey.PubKey }, new[] { requester }, network, changeOutputAddress, new[] { blinded }, inputsProofs, BackendHttpClient);
            }

            await WaitForTimeoutAsync();

            int bannedCount = coordinator.UtxoReferee.CountBanned(false);

            Assert.Equal(0, bannedCount);
            int notedCount = coordinator.UtxoReferee.CountBanned(true);

            Assert.Equal(anonymitySet, notedCount);

            round = coordinator.GetCurrentInputRegisterableRoundOrDefault();

            foreach (var registerRequest in registerRequests)
            {
                await AliceClientBase.CreateNewAsync(round.RoundId, aliceClientBackup.RegisteredAddresses, round.MixingLevels.GetAllLevels().Select(x => x.SignerKey.PubKey), aliceClientBackup.Requesters, network, registerRequest.changeOutputAddress, new[] { registerRequest.blindedData }, registerRequest.inputsProofs, BackendHttpClient);
            }

            await WaitForTimeoutAsync();

            bannedCount = coordinator.UtxoReferee.CountBanned(false);
            Assert.Equal(anonymitySet, bannedCount);
            notedCount = coordinator.UtxoReferee.CountBanned(true);
            Assert.Equal(anonymitySet, notedCount);
        }
Exemple #6
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();
                        }
                    }
                    changeAddress = changeAddress ?? KeyManager.GenerateNewKey("ZeroLink Change", KeyState.Locked, isInternal: true, toFile: false).GetP2wpkhAddress(Network);
                    activeAddress = activeAddress ?? KeyManager.GenerateNewKey("ZeroLink Mixed Coin", KeyState.Locked, isInternal: true, toFile: false).GetP2wpkhAddress(Network);
                    KeyManager.ToFile();

                    var blind = CoordinatorPubKey.Blind(activeAddress.ScriptPubKey.ToBytes());

                    var inputProofs = new List <InputProofModel>();
                    foreach ((uint256 txid, uint index)coinReference in registrableCoins)
                    {
                        var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin == null)
                        {
                            throw new NotSupportedException("This is impossible.");
                        }
                        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 == 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 == 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 ProcessStatusAsync()
        {
            try
            {
                IEnumerable <CcjRunningRoundState> states = await SatoshiClient.GetAllRoundStatesAsync();

                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.
                        {
                            foreach (MixCoin rc in round.CoinsRegistered)
                            {
                                CoinsWaitingForMix.Add(rc);
                            }
                            roundsToRemove.Add(round.State.RoundId);
                        }
                    }

                    foreach (long roundId in roundsToRemove)
                    {
                        Rounds.RemoveAll(x => x.State.RoundId == roundId);
                        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);

                using (await MixLock.LockAsync())
                {
                    CoinsWaitingForMix.RemoveAll(x => x.SmartCoin.SpenderTransactionId != null);                     // Make sure coins those were somehow spent are removed.

                    CcjClientRound inputRegistrableRound = Rounds.First(x => x.State.Phase == CcjRoundPhase.InputRegistration);
                    if (inputRegistrableRound.AliceUniqueId == null)                     // If didn't register already, check what can we register.
                    {
                        try
                        {
                            var   coinsToRegister             = new List <MixCoin>();
                            var   amountSoFar                 = Money.Zero;
                            Money amountNeededExceptInputFees = inputRegistrableRound.State.Denomination + inputRegistrableRound.State.FeePerOutputs * 2;
                            var   tooSmallInputs              = false;
                            foreach (MixCoin coin in CoinsWaitingForMix
                                     .Where(x => x.SmartCoin.Confirmed || x.SmartCoin.Label.Contains("CoinJoin", StringComparison.Ordinal)) // Where our label contains CoinJoin, CoinJoins can be registered even if not confirmed, our label will likely be CoinJoin only if it was a previous CoinJoin, otherwise the server will refuse us.
                                     .OrderByDescending(y => y.SmartCoin.Amount)                                                            // First order by amount.
                                     .OrderByDescending(z => z.SmartCoin.Confirmed))                                                        // Then order by the amount ordered ienumerable by confirmation, so first try to register confirmed coins.
                            {
                                coinsToRegister.Add(coin);
                                if (inputRegistrableRound.State.MaximumInputCountPerPeer < coinsToRegister.Count)
                                {
                                    tooSmallInputs = true;
                                    break;
                                }

                                amountSoFar += coin.SmartCoin.Amount;
                                if (amountSoFar > amountNeededExceptInputFees + inputRegistrableRound.State.FeePerInputs * coinsToRegister.Count)
                                {
                                    break;
                                }
                            }

                            // If input count doesn't reach the max input registration AND there are enough coins queued, then register to mix.
                            if (!tooSmallInputs && amountSoFar > amountNeededExceptInputFees + inputRegistrableRound.State.FeePerInputs * coinsToRegister.Count)
                            {
                                var changeKey = KeyManager.GenerateNewKey("CoinJoin Change Output", KeyState.Locked, isInternal: true);
                                var activeKey = KeyManager.GenerateNewKey("CoinJoin Active Output", KeyState.Locked, isInternal: true);
                                var blind     = BlindingPubKey.Blind(activeKey.GetP2wpkhScript().ToBytes());

                                var inputProofs = new List <InputProofModel>();
                                foreach (var coin in coinsToRegister)
                                {
                                    var inputProof = new InputProofModel
                                    {
                                        Input = coin.SmartCoin.GetOutPoint(),
                                        Proof = coin.Secret.PrivateKey.SignMessage(ByteHelpers.ToHex(blind.BlindedData))
                                    };
                                    inputProofs.Add(inputProof);
                                }
                                InputsResponse inputsResponse = await AliceClient.PostInputsAsync(changeKey.GetP2wpkhScript(), blind.BlindedData, inputProofs.ToArray());

                                if (!BlindingPubKey.Verify(inputsResponse.BlindedOutputSignature, blind.BlindedData))
                                {
                                    throw new NotSupportedException("Coordinator did not sign the blinded output properly.");
                                }

                                CcjClientRound roundRegistered = Rounds.SingleOrDefault(x => x.State.RoundId == inputsResponse.RoundId);
                                if (roundRegistered == 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, inputsResponse.RoundId, registeredPeerCount: 1));
                                    Rounds.Add(roundRegistered);
                                    RoundAdded?.Invoke(this, roundRegistered);
                                }

                                foreach (var coin in coinsToRegister)
                                {
                                    roundRegistered.CoinsRegistered.Add(coin);
                                    CoinsWaitingForMix.Remove(coin);
                                }
                                roundRegistered.ActiveOutput       = activeKey;
                                roundRegistered.ChangeOutput       = changeKey;
                                roundRegistered.UnblindedSignature = BlindingPubKey.UnblindSignature(inputsResponse.BlindedOutputSignature, blind.BlindingFactor);
                                roundRegistered.AliceUniqueId      = inputsResponse.UniqueId;
                                RoundUpdated?.Invoke(this, roundRegistered);
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                    else                     // We registered, let's confirm we're online.
                    {
                        try
                        {
                            string roundHash = await AliceClient.PostConfirmationAsync(inputRegistrableRound.State.RoundId, (Guid)inputRegistrableRound.AliceUniqueId);

                            if (roundHash != null)                             // Then the phase went to connection confirmation.
                            {
                                inputRegistrableRound.RoundHash   = roundHash;
                                inputRegistrableRound.State.Phase = CcjRoundPhase.ConnectionConfirmation;
                                RoundUpdated?.Invoke(this, inputRegistrableRound);
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }

                    foreach (CcjClientRound ongoingRound in Rounds.Where(x => x.State.Phase != CcjRoundPhase.InputRegistration && x.AliceUniqueId != null))
                    {
                        try
                        {
                            if (ongoingRound.State.Phase == CcjRoundPhase.ConnectionConfirmation)
                            {
                                if (ongoingRound.RoundHash == null)                                 // If we didn't already obtained our roundHash obtain it.
                                {
                                    string roundHash = await AliceClient.PostConfirmationAsync(inputRegistrableRound.State.RoundId, (Guid)inputRegistrableRound.AliceUniqueId);

                                    if (roundHash == null)
                                    {
                                        throw new NotSupportedException("Coordinator didn't gave us the expected roundHash, even though it's in ConnectionConfirmation phase.");
                                    }
                                    else
                                    {
                                        ongoingRound.RoundHash = roundHash;
                                        RoundUpdated?.Invoke(this, ongoingRound);
                                    }
                                }
                            }
                            else if (ongoingRound.State.Phase == CcjRoundPhase.OutputRegistration)
                            {
                                if (ongoingRound.RoundHash == null)
                                {
                                    throw new NotSupportedException("Coordinator progressed to OutputRegistration phase, even though we didn't obtain roundHash.");
                                }

                                await BobClient.PostOutputAsync(ongoingRound.RoundHash, ongoingRound.ActiveOutput.GetP2wpkhScript(), ongoingRound.UnblindedSignature);
                            }
                            else if (ongoingRound.State.Phase == CcjRoundPhase.Signing)
                            {
                                Transaction unsignedCoinJoin = await AliceClient.GetUnsignedCoinJoinAsync(ongoingRound.State.RoundId, (Guid)ongoingRound.AliceUniqueId);

                                if (NBitcoinHelpers.HashOutpoints(unsignedCoinJoin.Inputs.Select(x => x.PrevOut)) != ongoingRound.RoundHash)
                                {
                                    throw new NotSupportedException("Coordinator provided invalid roundHash.");
                                }
                                Money amountBack = unsignedCoinJoin.Outputs
                                                   .Where(x => x.ScriptPubKey == ongoingRound.ActiveOutput.GetP2wpkhScript() || x.ScriptPubKey == ongoingRound.ChangeOutput.GetP2wpkhScript())
                                                   .Sum(y => y.Value);
                                Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.SmartCoin.Amount);                                              // Start with input sum.
                                minAmountBack -= ongoingRound.State.FeePerOutputs * 2 + ongoingRound.State.FeePerInputs * ongoingRound.CoinsRegistered.Count; // Minus miner fee.
                                Money actualDenomination     = unsignedCoinJoin.GetIndistinguishableOutputs().OrderByDescending(x => x.count).First().value;  // Denomination may grow.
                                Money expectedCoordinatorFee = new Money((ongoingRound.State.CoordinatorFeePercent * 0.01m) * decimal.Parse(actualDenomination.ToString(false, true)), MoneyUnit.BTC);
                                minAmountBack -= expectedCoordinatorFee;                                                                                      // Minus expected coordinator fee.

                                // If there's no change output then coordinator protection may happened:
                                if (unsignedCoinJoin.Outputs.All(x => x.ScriptPubKey != ongoingRound.ChangeOutput.GetP2wpkhScript()))
                                {
                                    Money minimumOutputAmount      = new Money(0.0001m, MoneyUnit.BTC);                                             // If the change would be less than about $1 then add it to the coordinator.
                                    Money onePercentOfDenomination = new Money(actualDenomination.ToDecimal(MoneyUnit.BTC) * 0.01m, MoneyUnit.BTC); // If the change is less than about 1% of the newDenomination then add it to the coordinator fee.
                                    Money minimumChangeAmount      = Math.Max(minimumOutputAmount, onePercentOfDenomination);

                                    minAmountBack -= minimumChangeAmount;                                     // Minus coordinator protections (so it won't create bad coinjoins.)
                                }

                                if (amountBack < minAmountBack)
                                {
                                    throw new NotSupportedException("Coordinator did not add enough value to our outputs in the coinjoin.");
                                }

                                new TransactionBuilder()
                                .AddKeys(ongoingRound.CoinsRegistered.Select(x => x.Secret).ToArray())
                                .AddCoins(ongoingRound.CoinsRegistered.Select(x => x.SmartCoin.GetCoin()))
                                .SignTransactionInPlace(unsignedCoinJoin, SigHash.All);

                                var myDic = new Dictionary <int, WitScript>();

                                for (int i = 0; i < unsignedCoinJoin.Inputs.Count; i++)
                                {
                                    var input = unsignedCoinJoin.Inputs[i];
                                    if (ongoingRound.CoinsRegistered.Select(x => x.SmartCoin.GetOutPoint()).Contains(input.PrevOut))
                                    {
                                        myDic.Add(i, unsignedCoinJoin.Inputs[i].WitScript);
                                    }
                                }

                                await AliceClient.PostSignaturesAsync(ongoingRound.State.RoundId, (Guid)ongoingRound.AliceUniqueId, myDic);
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError <CcjClient>(ex);
                        }
                    }
                }
            }
            catch (TaskCanceledException ex)
            {
                Logger.LogTrace <CcjClient>(ex);
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }