public IActionResult PostUnconfimation([FromQuery, Required] string uniqueId, [FromQuery, Required] long roundId)
        {
            if (roundId < 0 || !ModelState.IsValid)
            {
                return(BadRequest());
            }

            Guid uniqueIdGuid = GetGuidOrFailureResponse(uniqueId, out IActionResult returnFailureResponse);

            if (returnFailureResponse != null)
            {
                return(returnFailureResponse);
            }

            CoordinatorRound round = Coordinator.TryGetRound(roundId);

            if (round is null)
            {
                return(Ok("Round not found."));
            }

            Alice alice = round.TryGetAliceBy(uniqueIdGuid);

            if (alice is null)
            {
                return(Ok("Alice not found."));
            }

            if (round.Status != CoordinatorRoundStatus.Running)
            {
                return(Gone("Round is not running."));
            }

            RoundPhase phase = round.Phase;

            switch (phase)
            {
            case RoundPhase.InputRegistration:
            {
                round.RemoveAlicesBy(uniqueIdGuid);
                return(NoContent());
            }

            default:
            {
                return(Gone($"Participation can be only unconfirmed from InputRegistration phase. Current phase: {phase}."));
            }
            }
        }
Exemplo n.º 2
0
        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())
            {
                CoordinatorRound round = Coordinator.TryGetRound(request.RoundId);

                if (round is null || round.Phase != RoundPhase.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 <OutPoint>();
                    foreach (InputProofModel inputProof in request.Inputs)
                    {
                        var outpoint = inputProof.Input.ToOutPoint();
                        if (uniqueInputs.Contains(outpoint))
                        {
                            return(BadRequest("Cannot register an input twice."));
                        }
                        uniqueInputs.Add(outpoint);
                    }

                    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.ContainsUnconfirmedCoinJoinAsync(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 != CoordinatorRoundStatus.Running || round.Phase != RoundPhase.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(RoundPhase.ConnectionConfirmation);
                        }
                    }

                    var resp = new InputsResponse
                    {
                        UniqueId = alice.UniqueId,
                        RoundId  = round.RoundId
                    };
                    return(Ok(resp));
                }
                catch (Exception ex)
                {
                    Logger.LogDebug(ex);
                    return(BadRequest(ex.Message));
                }
            }
        }