Beispiel #1
0
        public async Task MakeSureTwoRunningRoundsAsync(Money feePerInputs = null, Money feePerOutputs = null)
        {
            using (await RoundsListLock.LockAsync())
            {
                int runningRoundCount = Rounds.Count(x => x.Status == CcjRoundStatus.Running);

                int confirmationTarget = await AdjustConfirmationTargetAsync(lockCoinJoins : true);

                if (runningRoundCount == 0)
                {
                    var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig, confirmationTarget);
                    round.CoinJoinBroadcasted += Round_CoinJoinBroadcasted;
                    round.StatusChanged       += Round_StatusChangedAsync;
                    await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration, feePerInputs, feePerOutputs);

                    Rounds.Add(round);

                    var round2 = new CcjRound(RpcClient, UtxoReferee, RoundConfig, confirmationTarget);
                    round2.StatusChanged       += Round_StatusChangedAsync;
                    round2.CoinJoinBroadcasted += Round_CoinJoinBroadcasted;
                    await round2.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration, feePerInputs, feePerOutputs);

                    Rounds.Add(round2);
                }
                else if (runningRoundCount == 1)
                {
                    var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig, confirmationTarget);
                    round.StatusChanged       += Round_StatusChangedAsync;
                    round.CoinJoinBroadcasted += Round_CoinJoinBroadcasted;
                    await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration, feePerInputs, feePerOutputs);

                    Rounds.Add(round);
                }
            }
        }
        public async Task <IActionResult> PostOutputAsync([FromQuery] string roundHash, [FromBody] OutputRequest outputRequest)
        {
            if (string.IsNullOrWhiteSpace(roundHash) ||
                outputRequest == null ||
                string.IsNullOrWhiteSpace(outputRequest.OutputScript) ||
                string.IsNullOrWhiteSpace(outputRequest.SignatureHex) ||
                !ModelState.IsValid)
            {
                return(BadRequest());
            }

            CcjRound round = Coordinator.TryGetRound(roundHash);

            if (round == null)
            {
                return(NotFound("Round not found."));
            }

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

            CcjRoundPhase phase = round.Phase;

            if (phase != CcjRoundPhase.OutputRegistration)
            {
                return(Forbid($"Output registration can only be done from OutputRegistration phase. Current phase: {phase}."));
            }

            var outputScript = new Script(outputRequest.OutputScript);

            if (RsaKey.PubKey.Verify(ByteHelpers.FromHex(outputRequest.SignatureHex), outputScript.ToBytes()))
            {
                using (await OutputLock.LockAsync())
                {
                    Bob bob = null;
                    try
                    {
                        bob = new Bob(outputScript);
                        round.AddBob(bob);
                    }
                    catch (Exception ex)
                    {
                        return(BadRequest($"Invalid outputScript is provided. Details: {ex.Message}"));
                    }

                    if (round.CountBobs() == round.AnonymitySet)
                    {
                        await round.ExecuteNextPhaseAsync(CcjRoundPhase.Signing);
                    }
                }

                return(NoContent());
            }
            else
            {
                return(BadRequest("Invalid signature provided."));
            }
        }
        public async Task MakeSureTwoRunningRoundsAsync()
        {
            using (await RoundsListLock.LockAsync())
            {
                int runningRoundCount = Rounds.Count(x => x.Status == CcjRoundStatus.Running);
                if (runningRoundCount == 0)
                {
                    var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
                    round.StatusChanged += Round_StatusChangedAsync;
                    await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);

                    Rounds.Add(round);

                    var round2 = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
                    round2.StatusChanged += Round_StatusChangedAsync;
                    await round2.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);

                    Rounds.Add(round2);
                }
                else if (runningRoundCount == 1)
                {
                    var round = new CcjRound(RpcClient, UtxoReferee, RoundConfig);
                    round.StatusChanged += Round_StatusChangedAsync;
                    await round.ExecuteNextPhaseAsync(CcjRoundPhase.InputRegistration);

                    Rounds.Add(round);
                }
            }
        }
        private (CcjRound round, Alice alice) GetRunningRoundAndAliceOrFailureResponse(long roundId, string uniqueId, out IActionResult returnFailureResponse)
        {
            returnFailureResponse = null;

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

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

            CcjRound round = Coordinator.TryGetRound(roundId);

            if (round == null)
            {
                returnFailureResponse = NotFound("Round not found.");
                return(null, null);
            }

            Alice alice = round.TryGetAliceBy(uniqueIdGuid);

            if (alice == null)
            {
                returnFailureResponse = NotFound("Alice not found.");
                return(round, null);
            }

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

            return(round, alice);
        }
Beispiel #5
0
        /// <summary>
        /// Depending on the number of unconfirmed coinjoins lower the confirmation target.
        /// https://github.com/zkSNACKs/WalletWasabi/issues/1155
        /// </summary>
        private async Task <int> AdjustConfirmationTargetAsync(bool lockCoinJoins)
        {
            try
            {
                uint256[] mempoolHashes = await RpcClient.GetRawMempoolAsync();

                int unconfirmedCoinJoinsCount = 0;
                if (lockCoinJoins)
                {
                    using (await CoinJoinsLock.LockAsync())
                    {
                        unconfirmedCoinJoinsCount = CoinJoins.Intersect(mempoolHashes).Count();
                    }
                }
                else
                {
                    unconfirmedCoinJoinsCount = CoinJoins.Intersect(mempoolHashes).Count();
                }

                int confirmationTarget = CcjRound.AdjustConfirmationTarget(unconfirmedCoinJoinsCount, RoundConfig.ConfirmationTarget.Value, RoundConfig.ConfirmationTargetReductionRate.Value);
                return(confirmationTarget);
            }
            catch (Exception ex)
            {
                Logger.LogWarning <CcjCoordinator>("Adjusting confirmation target failed. Falling back to default, specified in config.");
                Logger.LogWarning <CcjCoordinator>(ex);

                return(RoundConfig.ConfirmationTarget.Value);
            }
        }
Beispiel #6
0
        private async void Round_StatusChangedAsync(object sender, CcjRoundStatus status)
        {
            var round = sender as CcjRound;

            Money feePerInputs  = null;
            Money feePerOutputs = null;

            // If success save the coinjoin.
            if (status == CcjRoundStatus.Succeded)
            {
                using (await CoinJoinsLock.LockAsync())
                {
                    uint256 coinJoinHash = round.SignedCoinJoin.GetHash();
                    CoinJoins.Add(coinJoinHash);
                    await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() });

                    // When a round succeeded, adjust the denomination as to users still be able to register with the latest round's active output amount.
                    IEnumerable <(Money value, int count)> outputs = round.SignedCoinJoin.GetIndistinguishableOutputs();
                    var bestOutput = outputs.OrderByDescending(x => x.count).FirstOrDefault();
                    if (bestOutput != default)
                    {
                        Money activeOutputAmount = bestOutput.value;

                        var fees = await CcjRound.CalculateFeesAsync(RpcClient, RoundConfig.ConfirmationTarget.Value);

                        feePerInputs  = fees.feePerInputs;
                        feePerOutputs = fees.feePerOutputs;

                        Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + 2 * feePerOutputs);
                        if (newDenominationToGetInWithactiveOutputs < RoundConfig.Denomination)
                        {
                            if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m))
                            {
                                RoundConfig.Denomination = newDenominationToGetInWithactiveOutputs;
                                await RoundConfig.ToFileAsync();
                            }
                        }
                    }
                }
            }

            // If aborted in signing phase, then ban Alices those didn't sign.
            if (status == CcjRoundStatus.Aborted && round.Phase == CcjRoundPhase.Signing)
            {
                foreach (Alice alice in round.GetAlicesByNot(AliceState.SignedCoinJoin, syncLock: false))                 // Because the event sometimes is raised from inside the lock.
                {
                    // If its from any coinjoin, then don't ban.
                    IEnumerable <OutPoint> utxosToBan = alice.Inputs.Select(x => x.Outpoint);
                    await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted : false, round.RoundId, utxosToBan.ToArray());
                }
            }

            // If finished start a new round.
            if (status == CcjRoundStatus.Aborted || status == CcjRoundStatus.Succeded)
            {
                round.StatusChanged       -= Round_StatusChangedAsync;
                round.CoinJoinBroadcasted -= Round_CoinJoinBroadcasted;
                await MakeSureTwoRunningRoundsAsync(feePerInputs, feePerOutputs);
            }
        }
        public IActionResult PostUncorfimation([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(Ok("Round not found."));
            }

            Alice alice = round.TryGetAliceBy(uniqueIdGuid);

            if (round == null)
            {
                return(Ok("Alice not found."));
            }

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

            CcjRoundPhase phase = round.Phase;

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

            default:
            {
                return(Forbid($"Participation can be only unconfirmed from InputRegistration phase. Current phase: {phase}."));
            }
            }
        }
Beispiel #8
0
        public IActionResult GetCoinJoin([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."));
            }

            if (round.TryGetAliceBy(uniqueIdGuid) == null)             // We don't care about Alice now, but let's not give the CJ to anyone who isn't participating.
            {
                return(NotFound("Alice not found."));
            }

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

            CcjRoundPhase phase = round.Phase;

            switch (phase)
            {
            case CcjRoundPhase.Signing:
            {
                return(Ok(round.GetUnsignedCoinJoinHex()));
            }

            default:
            {
                return(Conflict($"CoinJoin can only be requested from Signing phase. Current phase: {phase}."));
            }
            }
        }
Beispiel #9
0
        private async void Round_StatusChangedAsync(object sender, CcjRoundStatus status)
        {
            try
            {
                var round = sender as CcjRound;

                Money feePerInputs  = null;
                Money feePerOutputs = null;

                // If success save the coinjoin.
                if (status == CcjRoundStatus.Succeded)
                {
                    using (await CoinJoinsLock.LockAsync())
                    {
                        uint256 coinJoinHash = round.SignedCoinJoin.GetHash();
                        CoinJoins.Add(coinJoinHash);
                        await File.AppendAllLinesAsync(CoinJoinsFilePath, new[] { coinJoinHash.ToString() });

                        // When a round succeeded, adjust the denomination as to users still be able to register with the latest round's active output amount.
                        IEnumerable <(Money value, int count)> outputs = round.SignedCoinJoin.GetIndistinguishableOutputs(includeSingle: true);
                        var bestOutput = outputs.OrderByDescending(x => x.count).FirstOrDefault();
                        if (bestOutput != default)
                        {
                            Money activeOutputAmount = bestOutput.value;

                            int currentConfirmationTarget = await AdjustConfirmationTargetAsync(lockCoinJoins : false);

                            var fees = await CcjRound.CalculateFeesAsync(RpcClient, currentConfirmationTarget);

                            feePerInputs  = fees.feePerInputs;
                            feePerOutputs = fees.feePerOutputs;

                            Money newDenominationToGetInWithactiveOutputs = activeOutputAmount - (feePerInputs + 2 * feePerOutputs);
                            if (newDenominationToGetInWithactiveOutputs < RoundConfig.Denomination)
                            {
                                if (newDenominationToGetInWithactiveOutputs > Money.Coins(0.01m))
                                {
                                    RoundConfig.Denomination = newDenominationToGetInWithactiveOutputs;
                                    await RoundConfig.ToFileAsync();
                                }
                            }
                        }
                    }
                }

                // If aborted in signing phase, then ban Alices those didn't sign.
                if (status == CcjRoundStatus.Aborted && round.Phase == CcjRoundPhase.Signing)
                {
                    IEnumerable <Alice> alicesDidntSign = round.GetAlicesByNot(AliceState.SignedCoinJoin, syncLock: false);

                    CcjRound nextRound = GetCurrentInputRegisterableRoundOrDefault(syncLock: false);

                    if (nextRound != null)
                    {
                        int nextRoundAlicesCount = nextRound.CountAlices(syncLock: false);
                        var alicesSignedCount    = round.AnonymitySet - alicesDidntSign.Count();

                        // New round's anonset should be the number of alices those signed in this round.
                        // Except if the number of alices in the next round is already larger.
                        var newAnonymitySet = Math.Max(alicesSignedCount, nextRoundAlicesCount);
                        // But it cannot be larger than the current anonset of that round.
                        newAnonymitySet = Math.Min(newAnonymitySet, nextRound.AnonymitySet);

                        // Only change the anonymity set of the next round if new anonset doesnt equal and newanonset larger than 1.
                        if (nextRound.AnonymitySet != newAnonymitySet && newAnonymitySet > 1)
                        {
                            nextRound.UpdateAnonymitySet(newAnonymitySet, syncLock: false);

                            if (nextRoundAlicesCount >= nextRound.AnonymitySet)
                            {
                                // Progress to the next phase, which will be OutputRegistration
                                await nextRound.ExecuteNextPhaseAsync(CcjRoundPhase.ConnectionConfirmation);
                            }
                        }
                    }

                    foreach (Alice alice in alicesDidntSign)                     // Because the event sometimes is raised from inside the lock.
                    {
                        // If its from any coinjoin, then don't ban.
                        IEnumerable <OutPoint> utxosToBan = alice.Inputs.Select(x => x.Outpoint);
                        await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, forceNoted : false, round.RoundId, utxosToBan.ToArray());
                    }
                }

                // If finished start a new round.
                if (status == CcjRoundStatus.Aborted || status == CcjRoundStatus.Succeded)
                {
                    round.StatusChanged       -= Round_StatusChangedAsync;
                    round.CoinJoinBroadcasted -= Round_CoinJoinBroadcasted;
                    await MakeSureTwoRunningRoundsAsync(feePerInputs, feePerOutputs);
                }
            }
            catch (Exception ex)
            {
                Logger.LogWarning <CcjCoordinator>(ex);
            }
        }
        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}."));
            }
            }
        }
Beispiel #12
0
        public async Task <IActionResult> PostOutputAsync([FromQuery, Required] long roundId, [FromBody, Required] OutputRequest request)
        {
            if (roundId < 0 ||
                request.Level < 0 ||
                !ModelState.IsValid)
            {
                return(BadRequest());
            }

            CcjRound round = Coordinator.TryGetRound(roundId);

            if (round is null)
            {
                TryLogLateRequest(roundId, CcjRoundPhase.OutputRegistration);
                return(NotFound("Round not found."));
            }

            if (round.Status != CcjRoundStatus.Running)
            {
                TryLogLateRequest(roundId, CcjRoundPhase.OutputRegistration);
                return(Gone("Round is not running."));
            }

            CcjRoundPhase phase = round.Phase;

            if (phase != CcjRoundPhase.OutputRegistration)
            {
                TryLogLateRequest(roundId, CcjRoundPhase.OutputRegistration);
                return(Conflict($"Output registration can only be done from OutputRegistration phase. Current phase: {phase}."));
            }

            if (request.OutputAddress.Network != Network)
            {
                // RegTest and TestNet address formats are sometimes the same.
                if (Network == Network.Main)
                {
                    return(BadRequest($"Invalid OutputAddress Network."));
                }
            }

            if (request.OutputAddress == Constants.GetCoordinatorAddress(Network))
            {
                Logger.LogWarning <ChaumianCoinJoinController>($"Bob is registering the coordinator's address. Address: {request.OutputAddress}, Level: {request.Level}, Signature: {request.UnblindedSignature}.");
            }

            if (request.Level > round.MixingLevels.GetMaxLevel())
            {
                return(BadRequest($"Invalid mixing Level is provided. Provided: {request.Level}. Maximum: {round.MixingLevels.GetMaxLevel()}."));
            }

            if (round.ContainsRegisteredUnblindedSignature(request.UnblindedSignature))
            {
                return(NoContent());
            }

            MixingLevel mixinglevel = round.MixingLevels.GetLevel(request.Level);
            Signer      signer      = mixinglevel.Signer;

            if (signer.VerifyUnblindedSignature(request.UnblindedSignature, request.OutputAddress.ScriptPubKey.ToBytes()))
            {
                using (await OutputLock.LockAsync())
                {
                    Bob bob = null;
                    try
                    {
                        bob = new Bob(request.OutputAddress, mixinglevel);
                        round.AddBob(bob);
                        round.AddRegisteredUnblindedSignature(request.UnblindedSignature);
                    }
                    catch (Exception ex)
                    {
                        return(BadRequest($"Invalid outputAddress is provided. Details: {ex.Message}"));
                    }

                    int bobCount      = round.CountBobs();
                    int blindSigCount = round.CountBlindSignatures();
                    if (bobCount == blindSigCount)                     // If there'll be more bobs, then round failed. Someone may broke the crypto.
                    {
                        await round.ExecuteNextPhaseAsync(CcjRoundPhase.Signing);
                    }
                }

                return(NoContent());
            }
            return(BadRequest("Invalid signature provided."));
        }
Beispiel #13
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())
            {
                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));
                }
            }
        }
Beispiel #14
0
        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));
                }
            }
        }
Beispiel #15
0
        public async Task <IActionResult> PostSignaturesAsync([FromQuery] string uniqueId, [FromQuery] long roundId, [FromBody] IDictionary <int, string> signatures)
        {
            if (roundId <= 0 ||
                signatures == null ||
                signatures.Count() <= 0 ||
                signatures.Any(x => x.Key < 0 || string.IsNullOrWhiteSpace(x.Value)) ||
                !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 (alice == null)
            {
                return(NotFound("Alice not found."));
            }

            // Check if Alice provided signature to all her inputs.
            if (signatures.Count != alice.Inputs.Count())
            {
                return(BadRequest("Alice did not provide enough witnesses."));
            }

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

            CcjRoundPhase phase = round.Phase;

            switch (phase)
            {
            case CcjRoundPhase.Signing:
            {
                using (await SigningLock.LockAsync())
                {
                    foreach (var signaturePair in signatures)
                    {
                        int       index   = signaturePair.Key;
                        WitScript witness = null;
                        try
                        {
                            witness = new WitScript(signaturePair.Value);
                        }
                        catch (Exception ex)
                        {
                            return(BadRequest($"Malformed witness is provided. Details: {ex.Message}"));
                        }
                        int maxIndex = round.UnsignedCoinJoin.Inputs.Count - 1;
                        if (maxIndex < index)
                        {
                            return(BadRequest($"Index out of range. Maximum value: {maxIndex}. Provided value: {index}"));
                        }

                        // Check duplicates.
                        if (!string.IsNullOrWhiteSpace(round.SignedCoinJoin.Inputs[index].WitScript?.ToString()))                                        // Not sure why WitScript?.ToString() is needed, there was something wrong in previous HiddenWallet version if I didn't do this.
                        {
                            return(BadRequest($"Input is already signed."));
                        }

                        // Verify witness.
                        var cjCopy = new Transaction(round.UnsignedCoinJoin.ToHex());
                        cjCopy.Inputs[index].WitScript = witness;
                        TxOut output = alice.Inputs.Single(x => x.OutPoint == cjCopy.Inputs[index].PrevOut).Output;
                        if (!Script.VerifyScript(output.ScriptPubKey, cjCopy, index, output.Value, ScriptVerify.Standard, SigHash.All))
                        {
                            return(BadRequest($"Invalid witness is provided."));
                        }

                        // Finally add it to our CJ.
                        round.SignedCoinJoin.Inputs[index].WitScript = witness;
                    }

                    alice.State = AliceState.SignedCoinJoin;

                    await round.BroadcastCoinJoinIfFullySignedAsync();
                }

                return(NoContent());
            }

            default:
            {
                return(Conflict($"CoinJoin can only be requested from Signing phase. Current phase: {phase}."));
            }
            }
        }