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); } }
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); } }
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(); } }
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); }
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); } }