public async Task <IEnumerable <Alice> > RemoveAlicesIfInputsSpentAsync() { var alicesRemoved = new List <Alice>(); using (RoundSyncronizerLock.Lock()) { if ((Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation) || Status != CcjRoundStatus.Running) { throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases."); } foreach (Alice alice in Alices) { foreach (OutPoint input in alice.Inputs.Select(y => y.OutPoint)) { GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(input.Hash, (int)input.N, includeMempool : true); // Check if inputs are unspent. if (getTxOutResponse == null) { alicesRemoved.Add(alice); Alices.Remove(alice); } } } } foreach (var alice in alicesRemoved) { Logger.LogInfo <CcjRound>($"Round ({RoundId}): Alice ({alice.UniqueId}) removed."); } return(alicesRemoved); }
public async Task CanGetTxOutFromRPCAsync() { using (var builder = NodeBuilder.Create()) { var rpc = builder.CreateNode().CreateRPCClient(); builder.StartAll(); // 1. Generate some blocks and check if gettxout gives the right outputs for the first coin var blocksToGenerate = 101; uint256[] blockHashes = await rpc.GenerateAsync(blocksToGenerate); var txId = rpc.GetTransactions(blockHashes.First()).First().GetHash(); GetTxOutResponse getTxOutResponse = await rpc.GetTxOutAsync(txId, 0); Assert.NotNull(getTxOutResponse); // null if spent Assert.Equal(blockHashes.Last(), getTxOutResponse.BestBlock); Assert.Equal(getTxOutResponse.Confirmations, blocksToGenerate); Assert.Equal(Money.Coins(50), getTxOutResponse.TxOut.Value); Assert.NotNull(getTxOutResponse.TxOut.ScriptPubKey); Assert.Equal("pubkey", getTxOutResponse.ScriptPubKeyType); Assert.True(getTxOutResponse.IsCoinBase); // 2. Spend the first coin var address = new Key().PubKey.GetAddress(rpc.Network); Money sendAmount = Money.Parse("49"); txId = await rpc.SendToAddressAsync(address, sendAmount); // 3. Make sure if we don't include the mempool into the database the txo will not be considered utxo getTxOutResponse = await rpc.GetTxOutAsync(txId, 0, false); Assert.Null(getTxOutResponse); // 4. Find the output index we want to check var tx = rpc.GetRawTransaction(txId); int index = -1; for (int i = 0; i < tx.Outputs.Count; i++) { if (tx.Outputs[i].Value == sendAmount) { index = i; } } Assert.NotEqual(index, -1); // 5. Make sure the expected amounts are received for unconfirmed transactions getTxOutResponse = await rpc.GetTxOutAsync(txId, index, true); Assert.NotNull(getTxOutResponse); // null if spent Assert.Equal(blockHashes.Last(), getTxOutResponse.BestBlock); Assert.Equal(getTxOutResponse.Confirmations, 0); Assert.Equal(Money.Coins(49), getTxOutResponse.TxOut.Value); Assert.NotNull(getTxOutResponse.TxOut.ScriptPubKey); Assert.Equal("pubkeyhash", getTxOutResponse.ScriptPubKeyType); Assert.False(getTxOutResponse.IsCoinBase); } }
public UtxoReferee(Network network, string folderPath, RPCClient rpc, CoordinatorRoundConfig roundConfig) { Network = Guard.NotNull(nameof(network), network); FolderPath = Guard.NotNullOrEmptyOrWhitespace(nameof(folderPath), folderPath, trim: true); RpcClient = Guard.NotNull(nameof(rpc), rpc); RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig); BannedUtxos = new ConcurrentDictionary <OutPoint, BannedUtxo>(); Directory.CreateDirectory(FolderPath); if (File.Exists(BannedUtxosFilePath)) { try { var toRemove = new List <string>(); // what's been confirmed string[] allLines = File.ReadAllLines(BannedUtxosFilePath); foreach (string line in allLines) { var bannedRecord = BannedUtxo.FromString(line); GetTxOutResponse getTxOutResponse = RpcClient.GetTxOut(bannedRecord.Utxo.Hash, (int)bannedRecord.Utxo.N, includeMempool: true); // Check if inputs are unspent. if (getTxOutResponse is null) { toRemove.Add(line); } else { BannedUtxos.TryAdd(bannedRecord.Utxo, bannedRecord); } } if (toRemove.Count != 0) // a little performance boost, often it'll be empty { var newAllLines = allLines.Where(x => !toRemove.Contains(x)); File.WriteAllLines(BannedUtxosFilePath, newAllLines); } Logger.LogInfo($"{allLines.Length} banned UTXOs are loaded from {BannedUtxosFilePath}."); } catch (Exception ex) { Logger.LogWarning($"Banned UTXO file got corrupted. Deleting {BannedUtxosFilePath}. {ex.GetType()}: {ex.Message}"); File.Delete(BannedUtxosFilePath); } } else { Logger.LogInfo($"No banned UTXOs are loaded from {BannedUtxosFilePath}."); } }
public async Task <IActionResult> PostInputsAsync([FromBody] InputsRequest request) { // Validate request. if (!ModelState.IsValid || request == null || string.IsNullOrWhiteSpace(request.BlindedOutputScriptHex) || string.IsNullOrWhiteSpace(request.ChangeOutputScript) || request.Inputs == null || request.Inputs.Count() == 0 || request.Inputs.Any(x => x.Input == null || x.Input.Hash == null || string.IsNullOrWhiteSpace(x.Proof))) { return(BadRequest("Invalid request.")); } if (request.Inputs.Count() > 7) { return(BadRequest("Maximum 7 inputs can be registered.")); } using (await InputsLock.LockAsync()) { CcjRound round = Coordinator.GetCurrentInputRegisterableRound(); // Do more checks. try { if (round.ContainsBlindedOutputScriptHex(request.BlindedOutputScriptHex, out _)) { return(BadRequest("Blinded output has already been registered.")); } var changeOutput = new Script(request.ChangeOutputScript); var inputs = new HashSet <(OutPoint OutPoint, TxOut Output)>(); var alicesToRemove = new HashSet <Guid>(); foreach (InputProofModel inputProof in request.Inputs) { if (inputs.Any(x => x.OutPoint == inputProof.Input)) { return(BadRequest("Cannot register an input twice.")); } if (round.ContainsInput(inputProof.Input, out List <Alice> tr)) { alicesToRemove.UnionWith(tr.Select(x => x.UniqueId)); // Input is already registered by this alice, remove it later if all the checks are completed fine. } if (Coordinator.AnyRunningRoundContainsInput(inputProof.Input, out List <Alice> tnr)) { if (tr.Union(tnr).Count() > tr.Count()) { return(BadRequest("Input is already registered in another round.")); } } var bannedElem = Coordinator.UtxoReferee.BannedUtxos.SingleOrDefault(x => x.Key == inputProof.Input); if (bannedElem.Key != default) { int maxBan = (int)TimeSpan.FromDays(30).TotalMinutes; int banLeft = maxBan - (int)((DateTimeOffset.UtcNow - bannedElem.Value.timeOfBan).TotalMinutes); if (banLeft > 0) { return(BadRequest($"Input is banned from participation for {banLeft} minutes: {inputProof.Input.N}:{inputProof.Input.Hash}.")); } else { await Coordinator.UtxoReferee.UnbanAsync(bannedElem.Key); } } GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(inputProof.Input.Hash, (int)inputProof.Input.N, includeMempool : true); // Check if inputs are unspent. if (getTxOutResponse == null) { return(BadRequest("Provided input is not unspent.")); } // Check if unconfirmed. if (getTxOutResponse.Confirmations <= 0) { // If it spends a CJ then it may be acceptable to register. if (!Coordinator.ContainsCoinJoin(inputProof.Input.Hash)) { return(BadRequest("Provided input is neither confirmed, nor is from an unconfirmed coinjoin.")); } // After 24 unconfirmed cj in the mempool dont't let unconfirmed coinjoin to be registered. if (await Coordinator.IsUnconfirmedCoinJoinLimitReachedAsync()) { return(BadRequest("Provided input is from an unconfirmed coinjoin, but the maximum number of unconfirmed coinjoins is reached.")); } } // Check if immature. if (getTxOutResponse.Confirmations <= 100) { if (getTxOutResponse.IsCoinBase) { return(BadRequest("Provided input is immature.")); } } // Check if inputs are native segwit. if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash") { return(BadRequest("Provided input must be witness_v0_keyhash.")); } TxOut txout = getTxOutResponse.TxOut; var address = (BitcoinWitPubKeyAddress)txout.ScriptPubKey.GetDestinationAddress(Network); // Check if proofs are valid. bool validProof; try { validProof = address.VerifyMessage(request.BlindedOutputScriptHex, inputProof.Proof); } catch (FormatException ex) { return(BadRequest($"Provided proof is invalid: {ex.Message}")); } if (!validProof) { return(BadRequest("Provided proof is invalid.")); } inputs.Add((inputProof.Input, txout)); } // Check if inputs have enough coins. Money inputSum = inputs.Sum(x => x.Output.Value); Money networkFeeToPay = (inputs.Count() * round.FeePerInputs + 2 * round.FeePerOutputs); Money changeAmount = inputSum - (round.Denomination + networkFeeToPay); if (changeAmount < Money.Zero) { return(BadRequest($"Not enough inputs are provided. Fee to pay: {networkFeeToPay.ToString(false, true)} BTC. Round denomination: {round.Denomination.ToString(false, true)} BTC. Only provided: {inputSum.ToString(false, true)} BTC.")); } // Make sure Alice checks work. var alice = new Alice(inputs, networkFeeToPay, new Script(request.ChangeOutputScript), request.BlindedOutputScriptHex); foreach (Guid aliceToRemove in alicesToRemove) { round.RemoveAlicesBy(aliceToRemove); } round.AddAlice(alice); // All checks are good. Sign. byte[] blindedData; try { blindedData = ByteHelpers.FromHex(request.BlindedOutputScriptHex); } catch { return(BadRequest("Invalid blinded output hex.")); } Logger.LogDebug <ChaumianCoinJoinController>($"Blinded data hex: {request.BlindedOutputScriptHex}"); Logger.LogDebug <ChaumianCoinJoinController>($"Blinded data array size: {blindedData.Length}"); byte[] signature = RsaKey.SignBlindedData(blindedData); // Check if phase changed since. if (round.Status != ChaumianCoinJoin.CcjRoundStatus.Running || round.Phase != CcjRoundPhase.InputRegistration) { return(base.StatusCode(StatusCodes.Status503ServiceUnavailable, "The state of the round changed while handling the request. Try again.")); } // Progress round if needed. if (round.CountAlices() >= round.AnonymitySet) { await round.RemoveAlicesIfInputsSpentAsync(); if (round.CountAlices() >= round.AnonymitySet) { await round.ExecuteNextPhaseAsync(CcjRoundPhase.ConnectionConfirmation); } } var resp = new InputsResponse { UniqueId = alice.UniqueId, BlindedOutputSignature = signature, RoundId = round.RoundId }; return(Ok(resp)); } catch (Exception ex) { Logger.LogDebug <ChaumianCoinJoinController>(ex); return(BadRequest(ex.Message)); } } }
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 <IActionResult> PostInputsAsync([FromBody] InputsRequest request) { // Validate request. if (!ModelState.IsValid || request is null || string.IsNullOrWhiteSpace(request.BlindedOutputScriptHex) || string.IsNullOrWhiteSpace(request.ChangeOutputAddress) || request.Inputs is null || !request.Inputs.Any() || request.Inputs.Any(x => x.Input == default(TxoRef) || x.Input.TransactionId is null || string.IsNullOrWhiteSpace(x.Proof))) { return(BadRequest("Invalid request.")); } if (request.Inputs.Count() > 7) { return(BadRequest("Maximum 7 inputs can be registered.")); } using (await InputsLock.LockAsync()) { CcjRound round = Coordinator.GetCurrentInputRegisterableRound(); // Do more checks. try { if (round.ContainsBlindedOutputScriptHex(request.BlindedOutputScriptHex, out _)) { return(BadRequest("Blinded output has already been registered.")); } BitcoinAddress changeOutputAddress; try { changeOutputAddress = BitcoinAddress.Create(request.ChangeOutputAddress, Network); } catch (FormatException ex) { return(BadRequest($"Invalid ChangeOutputAddress. Details: {ex.Message}")); } var inputs = new HashSet <Coin>(); var alicesToRemove = new HashSet <Guid>(); foreach (InputProofModel inputProof in request.Inputs) { if (inputs.Any(x => x.Outpoint == inputProof.Input)) { return(BadRequest("Cannot register an input twice.")); } if (round.ContainsInput(inputProof.Input.ToOutPoint(), out List <Alice> tr)) { alicesToRemove.UnionWith(tr.Select(x => x.UniqueId)); // Input is already registered by this alice, remove it later if all the checks are completed fine. } if (Coordinator.AnyRunningRoundContainsInput(inputProof.Input.ToOutPoint(), out List <Alice> tnr)) { if (tr.Union(tnr).Count() > tr.Count()) { return(BadRequest("Input is already registered in another round.")); } } OutPoint outpoint = inputProof.Input.ToOutPoint(); var bannedElem = await Coordinator.UtxoReferee.TryGetBannedAsync(outpoint, notedToo : false); if (bannedElem != null) { return(BadRequest($"Input is banned from participation for {(int)bannedElem.Value.bannedRemaining.TotalMinutes} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.")); } GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(inputProof.Input.TransactionId, (int)inputProof.Input.Index, includeMempool : true); // Check if inputs are unspent. if (getTxOutResponse is null) { return(BadRequest($"Provided input is not unspent: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.")); } // Check if unconfirmed. if (getTxOutResponse.Confirmations <= 0) { // If it spends a CJ then it may be acceptable to register. if (!Coordinator.ContainsCoinJoin(inputProof.Input.TransactionId)) { return(BadRequest("Provided input is neither confirmed, nor is from an unconfirmed coinjoin.")); } // Check if mempool would accept a fake transaction created with the registered inputs. // This will catch ascendant/descendant count and size limits for example. var result = await RpcClient.TestMempoolAcceptAsync(new Coin(inputProof.Input.ToOutPoint(), getTxOutResponse.TxOut)); if (!result.accept) { return(BadRequest($"Provided input is from an unconfirmed coinjoin, but a limit is reached: {result.rejectReason}")); } } // Check if immature. if (getTxOutResponse.Confirmations <= 100) { if (getTxOutResponse.IsCoinBase) { return(BadRequest("Provided input is immature.")); } } // Check if inputs are native segwit. if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash") { return(BadRequest("Provided input must be witness_v0_keyhash.")); } TxOut txout = getTxOutResponse.TxOut; var address = (BitcoinWitPubKeyAddress)txout.ScriptPubKey.GetDestinationAddress(Network); // Check if proofs are valid. bool validProof; try { validProof = address.VerifyMessage(request.BlindedOutputScriptHex, inputProof.Proof); } catch (FormatException ex) { return(BadRequest($"Provided proof is invalid: {ex.Message}")); } if (!validProof) { await Coordinator.UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted : false, round.RoundId, outpoint); return(BadRequest("Provided proof is invalid.")); } inputs.Add(new Coin(inputProof.Input.ToOutPoint(), txout)); } // Check if inputs have enough coins. Money inputSum = inputs.Sum(x => x.Amount); Money networkFeeToPay = (inputs.Count() * round.FeePerInputs) + (2 * round.FeePerOutputs); Money changeAmount = inputSum - (round.Denomination + networkFeeToPay); if (changeAmount < Money.Zero) { return(BadRequest($"Not enough inputs are provided. Fee to pay: {networkFeeToPay.ToString(false, true)} BTC. Round denomination: {round.Denomination.ToString(false, true)} BTC. Only provided: {inputSum.ToString(false, true)} BTC.")); } // Make sure Alice checks work. var alice = new Alice(inputs, networkFeeToPay, changeOutputAddress, request.BlindedOutputScriptHex); foreach (Guid aliceToRemove in alicesToRemove) { round.RemoveAlicesBy(aliceToRemove); } round.AddAlice(alice); // All checks are good. Sign. byte[] blindedData; try { blindedData = ByteHelpers.FromHex(request.BlindedOutputScriptHex); } catch { return(BadRequest("Invalid blinded output hex.")); } byte[] signature = RsaKey.SignBlindedData(blindedData); // Check if phase changed since. if (round.Status != CcjRoundStatus.Running || round.Phase != CcjRoundPhase.InputRegistration) { return(base.StatusCode(StatusCodes.Status503ServiceUnavailable, "The state of the round changed while handling the request. Try again.")); } // Progress round if needed. if (round.CountAlices() >= round.AnonymitySet) { await round.RemoveAlicesIfAnInputRefusedByMempoolAsync(); if (round.CountAlices() >= round.AnonymitySet) { await round.ExecuteNextPhaseAsync(CcjRoundPhase.ConnectionConfirmation); } } var resp = new InputsResponse { UniqueId = alice.UniqueId, BlindedOutputSignature = signature, RoundId = round.RoundId }; return(Ok(resp)); } catch (Exception ex) { Logger.LogDebug <ChaumianCoinJoinController>(ex); return(BadRequest(ex.Message)); } } }