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 <IActionResult> PostConfirmationAsync([FromQuery] string uniqueId, [FromQuery] long roundId) { if (roundId <= 0 || !ModelState.IsValid) { return(BadRequest()); } Guid uniqueIdGuid = CheckUniqueId(uniqueId, out IActionResult returnFailureResponse); if (returnFailureResponse != null) { return(returnFailureResponse); } CcjRound round = Coordinator.TryGetRound(roundId); if (round == null) { return(NotFound("Round not found.")); } Alice alice = round.TryGetAliceBy(uniqueIdGuid); if (round == null) { return(NotFound("Alice not found.")); } if (round.Status != CcjRoundStatus.Running) { return(Forbid("Round is not running.")); } CcjRoundPhase phase = round.Phase; switch (phase) { case CcjRoundPhase.InputRegistration: { round.StartAliceTimeout(uniqueIdGuid); return(NoContent()); } case CcjRoundPhase.ConnectionConfirmation: { alice.State = AliceState.ConnectionConfirmed; // Progress round if needed. if (round.AllAlices(AliceState.ConnectionConfirmed)) { IEnumerable <Alice> alicesToBan = await round.RemoveAlicesIfInputsSpentAsync(); // So ban only those who confirmed participation, yet spent their inputs. if (alicesToBan.Count() > 0) { await Coordinator.UtxoReferee.BanUtxosAsync(1, DateTimeOffset.Now, alicesToBan.SelectMany(x => x.Inputs).Select(y => y.OutPoint).ToArray()); } int aliceCountAfterConnectionConfirmationTimeout = round.CountAlices(); if (aliceCountAfterConnectionConfirmationTimeout < 2) { round.Fail(); } else { round.UpdateAnonymitySet(aliceCountAfterConnectionConfirmationTimeout); // Progress to the next phase, which will be OutputRegistration await round.ExecuteNextPhaseAsync(CcjRoundPhase.OutputRegistration); } } return(Ok(round.RoundHash)); // Participation can be confirmed multiple times, whatever. } default: { return(Forbid($"Participation can be only confirmed from InputRegistration or ConnectionConfirmation phase. Current phase: {phase}.")); } } }