public async Task <IActionResult> PostInputsAsync([FromBody, Required] InputsRequest request) { // Validate request. if (request.RoundId < 0 || !ModelState.IsValid) { return(BadRequest("Invalid request.")); } if (request.Inputs.Count() > 7) { return(BadRequest("Maximum 7 inputs can be registered.")); } using (await InputsLock.LockAsync()) { CcjRound round = Coordinator.TryGetRound(request.RoundId); if (round is null || round.Phase != CcjRoundPhase.InputRegistration) { return(NotFound("No such running round in InputRegistration. Try another round.")); } // Do more checks. try { uint256[] blindedOutputs = request.BlindedOutputScripts.ToArray(); int blindedOutputCount = blindedOutputs.Length; int maxBlindedOutputCount = round.MixingLevels.Count(); if (blindedOutputCount > maxBlindedOutputCount) { return(BadRequest($"Too many blinded output was provided: {blindedOutputCount}, maximum: {maxBlindedOutputCount}.")); } if (blindedOutputs.Distinct().Count() < blindedOutputs.Length) { return(BadRequest("Duplicate blinded output found.")); } if (round.ContainsAnyBlindedOutputScript(blindedOutputs)) { return(BadRequest("Blinded output has already been registered.")); } if (request.ChangeOutputAddress.Network != Network) { // RegTest and TestNet address formats are sometimes the same. if (Network == Network.Main) { return(BadRequest($"Invalid ChangeOutputAddress Network.")); } } var uniqueInputs = new HashSet <TxoRef>(); foreach (InputProofModel inputProof in request.Inputs) { if (uniqueInputs.Contains(inputProof.Input)) { return(BadRequest("Cannot register an input twice.")); } uniqueInputs.Add(inputProof.Input); } var alicesToRemove = new HashSet <Guid>(); var getTxOutResponses = new List <(InputProofModel inputModel, Task <GetTxOutResponse> getTxOutTask)>(); var batch = RpcClient.PrepareBatch(); foreach (InputProofModel inputProof in request.Inputs) { 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.BannedRemaining.TotalMinutes} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}.")); } var txOutResponseTask = batch.GetTxOutAsync(inputProof.Input.TransactionId, (int)inputProof.Input.Index, includeMempool: true); getTxOutResponses.Add((inputProof, txOutResponseTask)); } // Perform all RPC request at once var waiting = Task.WhenAll(getTxOutResponses.Select(x => x.getTxOutTask)); await batch.SendBatchAsync(); await waiting; byte[] blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputs.Select(x => x.ToBytes())); uint256 blindedOutputScriptsHash = new uint256(Hashes.SHA256(blindedOutputScriptHashesByte)); var inputs = new HashSet <Coin>(); foreach (var responses in getTxOutResponses) { var(inputProof, getTxOutResponseTask) = responses; var getTxOutResponse = await getTxOutResponseTask; // 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 (!await Coordinator.ContainsCoinJoinAsync(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[] { 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. if (!address.VerifyMessage(blindedOutputScriptsHash, inputProof.Proof)) { return(BadRequest("Provided proof is invalid.")); } inputs.Add(new Coin(inputProof.Input.ToOutPoint(), txOut)); } var acceptedBlindedOutputScripts = new List <uint256>(); // Calculate expected networkfee to pay after base denomination. int inputCount = inputs.Count; Money networkFeeToPayAfterBaseDenomination = (inputCount * round.FeePerInputs) + (2 * round.FeePerOutputs); // Check if inputs have enough coins. Money inputSum = inputs.Sum(x => x.Amount); Money changeAmount = (inputSum - (round.MixingLevels.GetBaseDenomination() + networkFeeToPayAfterBaseDenomination)); if (changeAmount < Money.Zero) { return(BadRequest($"Not enough inputs are provided. Fee to pay: {networkFeeToPayAfterBaseDenomination.ToString(false, true)} BTC. Round denomination: {round.MixingLevels.GetBaseDenomination().ToString(false, true)} BTC. Only provided: {inputSum.ToString(false, true)} BTC.")); } acceptedBlindedOutputScripts.Add(blindedOutputs.First()); Money networkFeeToPay = networkFeeToPayAfterBaseDenomination; // Make sure we sign the proper number of additional blinded outputs. var moneySoFar = Money.Zero; for (int i = 1; i < blindedOutputCount; i++) { if (!round.MixingLevels.TryGetDenomination(i, out Money denomination)) { break; } Money coordinatorFee = denomination.Percentage(round.CoordinatorFeePercent * round.AnonymitySet); // It should be the number of bobs, but we must make sure they'd have money to pay all. changeAmount -= (denomination + round.FeePerOutputs + coordinatorFee); networkFeeToPay += round.FeePerOutputs; if (changeAmount < Money.Zero) { break; } acceptedBlindedOutputScripts.Add(blindedOutputs[i]); } // Make sure Alice checks work. var alice = new Alice(inputs, networkFeeToPayAfterBaseDenomination, request.ChangeOutputAddress, acceptedBlindedOutputScripts); foreach (Guid aliceToRemove in alicesToRemove) { round.RemoveAlicesBy(aliceToRemove); } round.AddAlice(alice); // All checks are good. Sign. var blindSignatures = new List <uint256>(); for (int i = 0; i < acceptedBlindedOutputScripts.Count; i++) { var blindedOutput = acceptedBlindedOutputScripts[i]; var signer = round.MixingLevels.GetLevel(i).Signer; uint256 blindSignature = signer.Sign(blindedOutput); blindSignatures.Add(blindSignature); } alice.BlindedOutputSignatures = blindSignatures.ToArray(); // Check if phase changed since. if (round.Status != CcjRoundStatus.Running || round.Phase != CcjRoundPhase.InputRegistration) { return(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, RoundId = round.RoundId }; return(Ok(resp)); } catch (Exception ex) { Logger.LogDebug <ChaumianCoinJoinController>(ex); return(BadRequest(ex.Message)); } } }