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