コード例 #1
0
        public async Task <IActionResult> PostInputsAsync([FromBody] InputsRequest request)
        {
            // Validate request.
            if (!ModelState.IsValid ||
                request == null ||
                string.IsNullOrWhiteSpace(request.BlindedOutputScriptHex) ||
                string.IsNullOrWhiteSpace(request.ChangeOutputAddress) ||
                request.Inputs == null ||
                !request.Inputs.Any() ||
                request.Inputs.Any(x => x.Input == default(TxoRef) ||
                                   x.Input.TransactionId == 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 <(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.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();
                        if (Coordinator.UtxoReferee.BannedUtxos.TryGetValue(outpoint, out (int severity, DateTimeOffset timeOfBan)bannedElem))
                        {
                            int maxBan  = (int)TimeSpan.FromDays(30).TotalMinutes;
                            int banLeft = maxBan - (int)((DateTimeOffset.UtcNow - bannedElem.timeOfBan).TotalMinutes);
                            if (banLeft > 0)
                            {
                                return(BadRequest($"Input is banned from participation for {banLeft} minutes: {inputProof.Input.Index}:{inputProof.Input.TransactionId}."));
                            }
                            await Coordinator.UtxoReferee.UnbanAsync(outpoint);
                        }

                        GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(inputProof.Input.TransactionId, (int)inputProof.Input.Index, 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.TransactionId))
                            {
                                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.ToOutPoint(), txout));
                    }

                    // Check if inputs have enough coins.
                    Money inputSum        = inputs.Select(x => x.Output.Value).Sum();
                    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.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));
                }
            }
        }
コード例 #2
0
        public async Task SignTransactionAsync()
        {
            WabiSabiConfig config = new();
            Round          round  = WabiSabiFactory.CreateRound(config);

            using Key key1 = new();
            Alice alice1 = WabiSabiFactory.CreateAlice(key: key1);

            round.Alices.Add(alice1);

            using Key key2 = new();
            Alice alice2 = WabiSabiFactory.CreateAlice(key: key2);

            round.Alices.Add(alice2);

            using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round);

            var mockRpc = new Mock <IRPCClient>();

            await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object);
            var wabiSabiApi = new WabiSabiController(coordinator);

            var rnd = new InsecureRandom();
            ZeroCredentialPool zeroAmountCredentials = new();
            ZeroCredentialPool zeroVsizeCredentials  = new();
            var amountClient = new WabiSabiClient(round.AmountCredentialIssuerParameters, rnd, 4300000000000ul, zeroAmountCredentials);
            var vsizeClient  = new WabiSabiClient(round.VsizeCredentialIssuerParameters, rnd, 2000ul, zeroVsizeCredentials);
            var apiClient    = new ArenaClient(amountClient, vsizeClient, wabiSabiApi);

            round.SetPhase(Phase.TransactionSigning);

            var emptyState = round.Assert <ConstructionState>();

            // We can't use ``emptyState.Finalize()` because this is not a valid transaction so we fake it
            var finalizedEmptyState = new SigningState(emptyState.Parameters, emptyState.Inputs, emptyState.Outputs);

            // No inputs in the CoinJoin.
            await Assert.ThrowsAsync <ArgumentException>(async() =>
                                                         await apiClient.SignTransactionAsync(round.Id, alice1.Coin, new BitcoinSecret(key1, Network.Main), finalizedEmptyState.CreateUnsignedTransaction(), CancellationToken.None));

            var oneInput = emptyState.AddInput(alice1.Coin).Finalize();

            round.CoinjoinState = oneInput;

            // Trying to sign coins those are not in the CoinJoin.
            await Assert.ThrowsAsync <InvalidOperationException>(async() =>
                                                                 await apiClient.SignTransactionAsync(round.Id, alice2.Coin, new BitcoinSecret(key2, Network.Main), oneInput.CreateUnsignedTransaction(), CancellationToken.None));

            var twoInputs = emptyState.AddInput(alice1.Coin).AddInput(alice2.Coin).Finalize();

            round.CoinjoinState = twoInputs;

            // Trying to sign coins with the wrong secret.
            await Assert.ThrowsAsync <InvalidOperationException>(async() =>
                                                                 await apiClient.SignTransactionAsync(round.Id, alice1.Coin, new BitcoinSecret(key2, Network.Main), twoInputs.CreateUnsignedTransaction(), CancellationToken.None));

            Assert.False(round.Assert <SigningState>().IsFullySigned);

            var unsigned = round.Assert <SigningState>().CreateUnsignedTransaction();

            await apiClient.SignTransactionAsync(round.Id, alice1.Coin, new BitcoinSecret(key1, Network.Main), unsigned, CancellationToken.None);

            Assert.True(round.Assert <SigningState>().IsInputSigned(alice1.Coin.Outpoint));
            Assert.False(round.Assert <SigningState>().IsInputSigned(alice2.Coin.Outpoint));

            Assert.False(round.Assert <SigningState>().IsFullySigned);

            await apiClient.SignTransactionAsync(round.Id, alice2.Coin, new BitcoinSecret(key2, Network.Main), unsigned, CancellationToken.None);

            Assert.True(round.Assert <SigningState>().IsInputSigned(alice2.Coin.Outpoint));

            Assert.True(round.Assert <SigningState>().IsFullySigned);
        }
コード例 #3
0
    protected void btnSave_Click(object sender, EventArgs e)
    {
        string    share_id  = Request.QueryString["share_id"];
        string    query     = "select cloud_id, file_name,fid from Share_Master where share_id='" + share_id + "'";
        DataTable dt        = Database.Getdata(query);
        string    cloud_id  = Convert.ToString(dt.Rows[0]["cloud_id"]);
        string    file_name = Convert.ToString(dt.Rows[0]["file_name"]);
        int       fileid    = Convert.ToInt32(dt.Rows[0]["fid"]);


        if (cloud_id == "Cloud")
        {
            SqlCommand     compare_keys = new SqlCommand("CompareKeys");
            var            sqltype      = CommandType.StoredProcedure;
            SqlParameter[] sqlpara      =
            {
                new SqlParameter("@key_value", txtsecret_key.Text)
            };

            DataTable key_output = Database.Getdata_Store(Database.cs, compare_keys, sqlpara, sqltype);
            if (key_output.Rows.Count > 0)
            {
                file_path     = Convert.ToString(key_output.Rows[0]["file_path"]);
                key           = txtsecret_key.Text;
                fileName      = Path.GetFileNameWithoutExtension(file_path);
                fileExtension = Path.GetExtension(file_path);

                ////////decryption

                //output = Server.MapPath("../Files/") + fileName + "_dec" + fileExtension;
                output     = fileName + "_dec" + fileExtension;
                input_file = Server.MapPath("../Files/" + file_path);

                SqlDataAdapter adp = new SqlDataAdapter("select * from File_Master where file_id='" + fileid + "'", Database.cs);
                DataTable      dt1 = new DataTable();
                adp.Fill(dt1);

                if (dt1.Rows.Count > 0)
                {
                    var keyBytes = (Byte[])dt1.Rows[0]["FileKey"];
                    var iv       = (Byte[])dt1.Rows[0]["IV"];

                    Byte[] fileBytes = Alice.Receive(File.ReadAllBytes(input_file), keyBytes, iv);

                    HttpResponse req = HttpContext.Current.Response;
                    req.AddHeader("Content-Disposition",
                                  "attachment;filename=\"" + output + "\"");
                    req.BinaryWrite(fileBytes);
                    req.End();

                    txtsecret_key.Text = "";
                    Response.Redirect("Download_File.aspx");
                }
            }
            else
            {
                Response.Write("<script>alert('invalid keys')</script>");
            }

            txtsecret_key.Text = "";
        }
        else
        {
            Response.Write("<script>alert('invalid keys.')</script>");
        }
    }
コード例 #4
0
        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 (alice == null)
            {
                return(NotFound("Alice not found."));
            }

            if (round.Status != CcjRoundStatus.Running)
            {
                return(Gone("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(Gone($"Participation can be only confirmed from InputRegistration or ConnectionConfirmation phase. Current phase: {phase}."));
            }
            }
        }
コード例 #5
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}."));
            }
            }
        }
コード例 #6
0
        public async Task <IActionResult> SignatureAsync([FromBody] SignatureRequest request)
        {
            try
            {
                if (Global.StateMachine.Phase != TumblerPhase.Signing || !Global.StateMachine.AcceptRequest)
                {
                    return(new ObjectResult(new FailureResponse {
                        Message = "Wrong phase"
                    }));
                }

                if (string.IsNullOrWhiteSpace(request.UniqueId))
                {
                    return(new BadRequestResult());
                }
                if (request.Signatures == null)
                {
                    return(new BadRequestResult());
                }
                if (request.Signatures.Count() == 0)
                {
                    return(new BadRequestResult());
                }
                Alice alice = Global.StateMachine.FindAlice(request.UniqueId, throwException: true);

                using (await SignatureProvidedAsyncLock.LockAsync())
                {
                    Transaction coinJoin = Global.StateMachine.CoinJoin;
                    foreach (var(Witness, Index) in request.Signatures)
                    {
                        var witness = new WitScript(Witness);
                        if (coinJoin.Inputs.Count <= Index)
                        {
                            // round fails, ban alice
                            await Global.UtxoReferee.BanAliceAsync(alice);

                            Global.StateMachine.FallBackRound = true;
                            Global.StateMachine.UpdatePhase(TumblerPhase.InputRegistration);
                            throw new ArgumentOutOfRangeException(nameof(Index));
                        }
                        coinJoin.Inputs[Index].WitScript = witness;
                        var output = alice.Inputs.Single(x => x.OutPoint == coinJoin.Inputs[Index].PrevOut).Output;
                        if (!Script.VerifyScript(output.ScriptPubKey, coinJoin, Index, output.Value, ScriptVerify.Standard, SigHash.All))
                        {
                            // round fails, ban alice
                            await Global.UtxoReferee.BanAliceAsync(alice);

                            Global.StateMachine.FallBackRound = true;
                            Global.StateMachine.UpdatePhase(TumblerPhase.InputRegistration);
                            throw new InvalidOperationException("Invalid witness");
                        }
                        else
                        {
                            Global.StateMachine.CoinJoin = coinJoin;
                        }
                    }

                    // check if fully signed
                    if (Global.StateMachine.FullySignedCoinJoin)
                    {
                        Console.WriteLine("Trying to propagate coinjoin....");
                        ConcurrentHashSet <Alice> alices = Global.StateMachine.Alices;
                        Coin[] spentCoins = alices.SelectMany(x => x.Inputs.Select(y => new Coin(y.OutPoint, y.Output))).ToArray();
                        var    fee        = coinJoin.GetFee(spentCoins);
                        Console.WriteLine($"Fee: {fee.ToString(false, true)}");
                        var feeRate = coinJoin.GetFeeRate(spentCoins);
                        Console.WriteLine($"FeeRate: {feeRate.FeePerK.ToDecimal(MoneyUnit.Satoshi) / 1000} satoshi/byte");

                        var state = CoinJoinTransactionState.Failed;
                        try
                        {
                            var res = await Global.RpcClient.SendCommandAsync(RPCOperations.sendrawtransaction, coinJoin.ToHex(), true);

                            if (coinJoin.GetHash().ToString() == res.ResultString)
                            {
                                state = CoinJoinTransactionState.Succeeded;
                                Console.WriteLine($"Propagated transaction: {coinJoin.GetHash()}");
                            }
                        }
                        finally
                        {
                            if (state == CoinJoinTransactionState.Failed)
                            {
                                Global.StateMachine.FallBackRound = true;
                            }
                            else
                            {
                                Global.StateMachine.FallBackRound = false;
                            }

                            Global.CoinJoinStore.Transactions.Add(new CoinJoinTransaction
                            {
                                Transaction = coinJoin,
                                DateTime    = DateTimeOffset.UtcNow,
                                State       = state
                            });
                            await Global.CoinJoinStore.ToFileAsync(Global.CoinJoinStorePath);

                            Global.StateMachine.UpdatePhase(TumblerPhase.InputRegistration);
                        }
                    }

                    alice.State = AliceState.SignedCoinJoin;
                    return(new ObjectResult(new SuccessResponse()));
                }
            }
            catch (Exception ex)
            {
                return(new ObjectResult(new FailureResponse {
                    Message = ex.Message
                }));
            }
        }
コード例 #7
0
        public static InputsRegistrationResponse RegisterInput(
            WabiSabiConfig config,
            Guid roundId,
            IDictionary <Coin, byte[]> coinRoundSignaturePairs,
            ZeroCredentialsRequest zeroAmountCredentialRequests,
            ZeroCredentialsRequest zeroWeightCredentialRequests,
            IDictionary <Guid, Round> rounds,
            Network network)
        {
            if (!rounds.TryGetValue(roundId, out var round))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
            }

            var alice = new Alice(coinRoundSignaturePairs);

            var coins = alice.Coins;

            if (round.MaxInputCountByAlice < coins.Count())
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooManyInputs);
            }
            if (round.IsBlameRound && coins.Select(x => x.Outpoint).Any(x => !round.BlameWhitelist.Contains(x)))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputNotWhitelisted);
            }

            var inputValueSum  = Money.Zero;
            var inputWeightSum = 0;

            foreach (var coinRoundSignaturePair in alice.CoinRoundSignaturePairs)
            {
                var coin      = coinRoundSignaturePair.Key;
                var signature = coinRoundSignaturePair.Value;

                var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData("CoinJoinCoordinatorIdentifier", round.Hash);
                if (!OwnershipProof.VerifyCoinJoinInputProof(signature, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData))
                {
                    throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongRoundSignature);
                }
                inputValueSum += coin.TxOut.Value;

                // Convert conservative P2WPKH size in virtual bytes to weight units.
                inputWeightSum += Constants.WitnessScaleFactor * coin.TxOut.ScriptPubKey.EstimateInputVsize();
            }

            if (inputValueSum < round.MinRegistrableAmount)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds);
            }
            if (inputValueSum > round.MaxRegistrableAmount)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds);
            }

            if (inputWeightSum > round.RegistrableWeightCredentials)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchWeight);
            }

            if (round.IsInputRegistrationEnded(config.MaxInputCountByRound, config.GetInputRegistrationTimeout(round)))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
            }

            var commitAmountCredentialResponse = round.AmountCredentialIssuer.PrepareResponse(zeroAmountCredentialRequests);
            var commitWeightCredentialResponse = round.WeightCredentialIssuer.PrepareResponse(zeroWeightCredentialRequests);

            RemoveDuplicateAlices(rounds, alice);

            alice.SetDeadlineRelativeTo(round.ConnectionConfirmationTimeout);
            round.Alices.Add(alice);

            return(new(alice.Id, commitAmountCredentialResponse(), commitWeightCredentialResponse()));
        }
コード例 #8
0
        public static InputsRegistrationResponse RegisterInput(
            WabiSabiConfig config,
            Guid roundId,
            IDictionary <Coin, byte[]> coinRoundSignaturePairs,
            ZeroCredentialsRequest zeroAmountCredentialRequests,
            ZeroCredentialsRequest zeroVsizeCredentialRequests,
            IDictionary <Guid, Round> rounds,
            Network network)
        {
            if (!rounds.TryGetValue(roundId, out var round))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
            }

            var coins = coinRoundSignaturePairs.Select(x => x.Key);

            if (round.MaxInputCountByAlice < coins.Count())
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooManyInputs);
            }
            if (round.IsBlameRound && coins.Select(x => x.Outpoint).Any(x => !round.BlameWhitelist.Contains(x)))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputNotWhitelisted);
            }

            foreach (var coinRoundSignaturePair in coinRoundSignaturePairs)
            {
                var coin      = coinRoundSignaturePair.Key;
                var signature = coinRoundSignaturePair.Value;

                var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData("CoinJoinCoordinatorIdentifier", round.Hash);
                if (!OwnershipProof.VerifyCoinJoinInputProof(signature, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData))
                {
                    throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongRoundSignature);
                }
            }

            var alice = new Alice(coinRoundSignaturePairs);

            if (alice.TotalInputAmount < round.MinRegistrableAmount)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds);
            }
            if (alice.TotalInputAmount > round.MaxRegistrableAmount)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds);
            }

            if (alice.TotalInputVsize > round.PerAliceVsizeAllocation)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchVsize);
            }

            if (round.IsInputRegistrationEnded(config.MaxInputCountByRound, config.GetInputRegistrationTimeout(round)))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
            }

            if (round.RemainingInputVsizeAllocation < round.PerAliceVsizeAllocation)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.VsizeQuotaExceeded);
            }
            var commitAmountCredentialResponse = round.AmountCredentialIssuer.PrepareResponse(zeroAmountCredentialRequests);
            var commitVsizeCredentialResponse  = round.VsizeCredentialIssuer.PrepareResponse(zeroVsizeCredentialRequests);

            RemoveDuplicateAlices(rounds, alice);

            alice.SetDeadlineRelativeTo(round.ConnectionConfirmationTimeout);
            round.Alices.Add(alice);

            return(new(alice.Id,
                       commitAmountCredentialResponse.Commit(),
                       commitVsizeCredentialResponse.Commit()));
        }
コード例 #9
0
 public void PublicSendMessageToAlice()
 {
     Alice.Create().FriendRecieveMessageFromBob <BobPrivateKey>(1337);
 }
コード例 #10
0
ファイル: TestBB84.cs プロジェクト: phaldon/QSI
 public Bob(Alice alice)
 {
     this.alice = alice;
     thread     = new Thread(Loop);
     thread.Start();
 }
コード例 #11
0
        private static void RemoveDuplicateAlices(IDictionary <Guid, Round> roundsWithId, Alice alice)
        {
            var rounds = roundsWithId.Values;

            if (rounds.Any(x => x.Phase != Phase.InputRegistration))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceAlreadyRegistered);
            }

            var aliceOutPoints = alice.Coins.Select(x => x.Outpoint).ToHashSet();
            var flattenTable   = rounds.SelectMany(x => x.Alices.SelectMany(y => y.Coins.Select(z => (Round: x, Alice: y, Output: z.Outpoint))));

            foreach (var(round, aliceInRound, _) in flattenTable.Where(x => aliceOutPoints.Contains(x.Output)).ToArray())
            {
                if (round.Alices.Remove(aliceInRound))
                {
                    Logger.LogInfo("Updated Alice registration.");
                }
            }
        }
コード例 #12
0
        public static InputRegistrationResponse RegisterInput(
            WabiSabiConfig config,
            uint256 roundId,
            Coin coin,
            OwnershipProof ownershipProof,
            ZeroCredentialsRequest zeroAmountCredentialRequests,
            ZeroCredentialsRequest zeroVsizeCredentialRequests,
            HashSet <Round> rounds)
        {
            if (rounds.FirstOrDefault(x => x.Id == roundId) is not Round round)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.RoundNotFound);
            }

            if (round.IsInputRegistrationEnded(config.MaxInputCountByRound, config.GetInputRegistrationTimeout(round)))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
            }

            if (round.IsBlameRound && !round.BlameWhitelist.Contains(coin.Outpoint))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputNotWhitelisted);
            }

            // Compute but don't commit updated CoinJoin to round state, it will
            // be re-calculated on input confirmation. This is computed it here
            // for validation purposes.
            round.Assert <ConstructionState>().AddInput(coin);

            var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData("CoinJoinCoordinatorIdentifier", round.Id);

            if (!OwnershipProof.VerifyCoinJoinInputProof(ownershipProof, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongOwnershipProof);
            }

            var alice = new Alice(coin, ownershipProof, round);

            if (alice.TotalInputAmount < round.MinRegistrableAmount)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds);
            }
            if (alice.TotalInputAmount > round.MaxRegistrableAmount)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds);
            }

            if (alice.TotalInputVsize > round.MaxVsizeAllocationPerAlice)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchVsize);
            }

            if (round.RemainingInputVsizeAllocation < round.MaxVsizeAllocationPerAlice)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.VsizeQuotaExceeded);
            }
            var commitAmountCredentialResponse = round.AmountCredentialIssuer.PrepareResponse(zeroAmountCredentialRequests);
            var commitVsizeCredentialResponse  = round.VsizeCredentialIssuer.PrepareResponse(zeroVsizeCredentialRequests);

            alice.SetDeadlineRelativeTo(round.ConnectionConfirmationTimeout);
            round.Alices.Add(alice);

            return(new(alice.Id,
                       commitAmountCredentialResponse.Commit(),
                       commitVsizeCredentialResponse.Commit()));
        }
コード例 #13
0
 public void Ban(Alice alice, uint256 lastDisruptedRoundId)
 {
     Punish(alice.Coin.Outpoint, Punishment.Banned, lastDisruptedRoundId);
 }
コード例 #14
0
        public async Task <IActionResult> InputsAsync([FromBody] InputsRequest request)
        {
            var roundId = Global.StateMachine.RoundId;

            TumblerPhase phase = TumblerPhase.InputRegistration;

            try
            {
                if (Global.StateMachine.Phase != TumblerPhase.InputRegistration || !Global.StateMachine.AcceptRequest)
                {
                    return(new ObjectResult(new FailureResponse {
                        Message = "Wrong phase"
                    }));
                }

                // Check not nulls
                string blindedOutputString = request.BlindedOutput.Trim();
                if (string.IsNullOrWhiteSpace(blindedOutputString))
                {
                    return(new BadRequestResult());
                }
                if (string.IsNullOrWhiteSpace(request.ChangeOutput))
                {
                    return(new BadRequestResult());
                }
                if (request.Inputs == null || request.Inputs.Count() == 0)
                {
                    return(new BadRequestResult());
                }

                // Check format (parse everyting))
                if (Global.StateMachine.BlindedOutputs.Contains(blindedOutputString))
                {
                    throw new ArgumentException("Blinded output has already been registered");
                }
                byte[]  blindedOutput = HexHelpers.GetBytes(blindedOutputString);
                Network network       = Global.Config.Network;
                var     changeOutput  = new BitcoinWitPubKeyAddress(request.ChangeOutput, expectedNetwork: network);
                if (request.Inputs.Count() > Global.Config.MaximumInputsPerAlices)
                {
                    throw new NotSupportedException("Too many inputs provided");
                }
                var inputs = new HashSet <(TxOut Output, OutPoint OutPoint)>();

                var alicesToRemove = new HashSet <Guid>();
                using (await InputRegistrationLock.LockAsync())
                {
                    foreach (InputProofModel input in request.Inputs)
                    {
                        var op = new OutPoint();
                        op.FromHex(input.Input);
                        if (inputs.Any(x => x.OutPoint.Hash == op.Hash && x.OutPoint.N == op.N))
                        {
                            throw new ArgumentException("Attempting to register an input twice is not permitted");
                        }
                        foreach (var a in Global.StateMachine.Alices)
                        {
                            if (a.Inputs.Any(x => x.OutPoint.Hash == op.Hash && x.OutPoint.N == op.N))
                            {
                                alicesToRemove.Add(a.UniqueId);                                 // input is already registered by this alice, remove it if all the checks are completed fine
                            }
                        }

                        BannedUtxo banned = Global.UtxoReferee.Utxos.FirstOrDefault(x => x.Utxo.Hash == op.Hash && x.Utxo.N == op.N);
                        if (banned != default(BannedUtxo))
                        {
                            var maxBan  = (int)TimeSpan.FromDays(30).TotalMinutes;
                            int banLeft = maxBan - (int)((DateTimeOffset.UtcNow - banned.TimeOfBan).TotalMinutes);
                            throw new ArgumentException($"Input is banned for {banLeft} minutes");
                        }

                        var getTxOutResponse = await Global.RpcClient.GetTxOutAsync(op.Hash, (int)op.N, true);

                        // Check if inputs are unspent
                        if (getTxOutResponse == null)
                        {
                            throw new ArgumentException("Provided input is not unspent");
                        }
                        // Check if inputs are unconfirmed, if so check if they are part of previous CoinJoin
                        if (getTxOutResponse.Confirmations <= 0)
                        {
                            if (!Global.CoinJoinStore.Transactions
                                .Any(x => x.State >= CoinJoinTransactionState.Succeeded && x.Transaction.GetHash() == op.Hash))
                            {
                                throw new ArgumentException("Provided input is not confirmed, nor spends a previous CJ transaction");
                            }
                            else
                            {
                                // after 24 unconfirmed cj in the mempool dont't let unconfirmed coinjoin to be registered
                                var unconfirmedCoinJoins = Global.CoinJoinStore.Transactions.Where(x => x.State == CoinJoinTransactionState.Succeeded);
                                if (unconfirmedCoinJoins.Count() >= 24)
                                {
                                    var toFailed    = new HashSet <uint256>();
                                    var toConfirmed = new HashSet <uint256>();
                                    foreach (var tx in unconfirmedCoinJoins)
                                    {
                                        RPCResponse getRawTransactionResponse = (await Global.RpcClient.SendCommandAsync("getrawtransaction", tx.Transaction.GetHash().ToString(), true));
                                        if (string.IsNullOrWhiteSpace(getRawTransactionResponse?.ResultString))
                                        {
                                            toFailed.Add(tx.Transaction.GetHash());
                                        }
                                        if (getRawTransactionResponse.Result.Value <int>("confirmations") > 0)
                                        {
                                            toConfirmed.Add(tx.Transaction.GetHash());
                                        }
                                    }
                                    foreach (var tx in toFailed)
                                    {
                                        Global.CoinJoinStore.TryUpdateState(tx, CoinJoinTransactionState.Failed);
                                    }
                                    foreach (var tx in toConfirmed)
                                    {
                                        Global.CoinJoinStore.TryUpdateState(tx, CoinJoinTransactionState.Confirmed);
                                    }
                                    if (toFailed.Count + toConfirmed.Count > 0)
                                    {
                                        await Global.CoinJoinStore.ToFileAsync(Global.CoinJoinStorePath);
                                    }
                                    // if couldn't remove any unconfirmed tx then refuse registration
                                    if (Global.CoinJoinStore.Transactions.Count(x => x.State == CoinJoinTransactionState.Succeeded) >= 24)
                                    {
                                        throw new ArgumentException("Registering unconfirmed CJ transaction output is currently not allowed due to too long mempool chain");
                                    }
                                }
                            }
                        }
                        // Check coinbase > 100
                        if (getTxOutResponse.Confirmations < 100)
                        {
                            if (getTxOutResponse.IsCoinBase)
                            {
                                throw new ArgumentException("Provided input is unspendable");
                            }
                        }
                        // Check if inputs are native segwit
                        if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash")
                        {
                            throw new ArgumentException("Provided input is not witness_v0_keyhash");
                        }

                        var txout = getTxOutResponse.TxOut;

                        var address = (BitcoinWitPubKeyAddress)txout.ScriptPubKey.GetDestinationAddress(network);
                        // Check if proofs are valid
                        var validProof = address.VerifyMessage(blindedOutputString, input.Proof);
                        if (!validProof)
                        {
                            throw new ArgumentException("Provided proof is invalid");
                        }

                        inputs.Add((txout, op));
                    }

                    // Check if inputs have enough coins
                    Money amount = Money.Zero;
                    foreach (Money val in inputs.Select(x => x.Output.Value))
                    {
                        amount += val;
                    }
                    Money feeToPay     = (inputs.Count() * Global.StateMachine.FeePerInputs + 2 * Global.StateMachine.FeePerOutputs);
                    Money changeAmount = amount - (Global.StateMachine.Denomination + feeToPay);
                    if (changeAmount < Money.Zero + new Money(548))                     // 546 is dust
                    {
                        throw new ArgumentException("Total provided inputs must be > denomination + fee + dust");
                    }

                    byte[] signature = Global.RsaKey.SignBlindedData(blindedOutput);
                    Global.StateMachine.BlindedOutputs.Add(blindedOutputString);

                    Guid uniqueId = Guid.NewGuid();

                    var alice = new Alice
                    {
                        UniqueId     = uniqueId,
                        ChangeOutput = changeOutput,
                        ChangeAmount = changeAmount,
                        State        = AliceState.InputsRegistered
                    };
                    alice.Inputs = new ConcurrentHashSet <(TxOut Output, OutPoint OutPoint)>();
                    foreach (var input in inputs)
                    {
                        alice.Inputs.Add(input);
                    }

                    AssertPhase(roundId, phase);
                    foreach (var aliceToRemove in alicesToRemove)
                    {
                        if (Global.StateMachine.TryRemoveAlice(aliceToRemove))
                        {
                            await Global.StateMachine.BroadcastPeerRegisteredAsync();
                        }
                    }
                    Global.StateMachine.Alices.Add(alice);

                    await Global.StateMachine.BroadcastPeerRegisteredAsync();

                    if (Global.StateMachine.Alices.Count >= Global.StateMachine.AnonymitySet)
                    {
                        Global.StateMachine.UpdatePhase(TumblerPhase.ConnectionConfirmation);
                    }
                    TumblerStateMachine.EstimateInputAndOutputSizes(out int inputSizeInBytes, out int outputSizeInBytes);
                    int estimatedTxSize = Global.StateMachine.Alices.SelectMany(x => x.Inputs).Count() * inputSizeInBytes + 2 * outputSizeInBytes;
                    if (estimatedTxSize >= 90000)                    // standard transaction is < 100KB
                    {
                        Global.StateMachine.UpdatePhase(TumblerPhase.ConnectionConfirmation);
                    }

                    var ret = new ObjectResult(new InputsResponse()
                    {
                        UniqueId            = uniqueId.ToString(),
                        SignedBlindedOutput = HexHelpers.ToString(signature)
                    });
                    return(ret);
                }
            }
            catch (Exception ex)
            {
                return(new ObjectResult(new FailureResponse {
                    Message = ex.Message
                }));
            }
        }
コード例 #15
0
    private async Task <InputRegistrationResponse> RegisterInputCoreAsync(InputRegistrationRequest request, CancellationToken cancellationToken)
    {
        var coin = await OutpointToCoinAsync(request, cancellationToken).ConfigureAwait(false);

        using (await AsyncLock.LockAsync(cancellationToken).ConfigureAwait(false))
        {
            var round = GetRound(request.RoundId);

            var registeredCoins = Rounds.Where(x => !(x.Phase == Phase.Ended && !x.WasTransactionBroadcast))
                                  .SelectMany(r => r.Alices.Select(a => a.Coin));

            if (registeredCoins.Any(x => x.Outpoint == coin.Outpoint))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceAlreadyRegistered);
            }

            if (round.IsInputRegistrationEnded(Config.MaxInputCountByRound))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase);
            }

            if (round is BlameRound blameRound && !blameRound.BlameWhitelist.Contains(coin.Outpoint))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputNotWhitelisted);
            }

            // Compute but don't commit updated coinjoin to round state, it will
            // be re-calculated on input confirmation. This is computed in here
            // for validation purposes.
            _ = round.Assert <ConstructionState>().AddInput(coin);

            var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData("CoinJoinCoordinatorIdentifier", round.Id);
            if (!OwnershipProof.VerifyCoinJoinInputProof(request.OwnershipProof, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongOwnershipProof);
            }

            // Generate a new GUID with the secure random source, to be sure
            // that it is not guessable (Guid.NewGuid() documentation does
            // not say anything about GUID version or randomness source,
            // only that the probability of duplicates is very low).
            var id = new Guid(Random.GetBytes(16));

            var isPayingZeroCoordinationFee = InMemoryCoinJoinIdStore.Contains(coin.Outpoint.Hash);

            if (!isPayingZeroCoordinationFee)
            {
                // If the coin comes from a tx that all of the tx inputs are coming from a CJ (1 hop - no pay).
                Transaction tx = await Rpc.GetRawTransactionAsync(coin.Outpoint.Hash, true, cancellationToken).ConfigureAwait(false);

                if (tx.Inputs.All(input => InMemoryCoinJoinIdStore.Contains(input.PrevOut.Hash)))
                {
                    isPayingZeroCoordinationFee = true;
                }
            }

            var alice = new Alice(coin, request.OwnershipProof, round, id, isPayingZeroCoordinationFee);

            if (alice.CalculateRemainingAmountCredentials(round.FeeRate, round.CoordinationFeeRate) <= Money.Zero)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.UneconomicalInput);
            }

            if (alice.TotalInputAmount < round.MinAmountCredentialValue)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds);
            }
            if (alice.TotalInputAmount > round.MaxAmountCredentialValue)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds);
            }

            if (alice.TotalInputVsize > round.MaxVsizeAllocationPerAlice)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchVsize);
            }

            var amountCredentialTask = round.AmountCredentialIssuer.HandleRequestAsync(request.ZeroAmountCredentialRequests, cancellationToken);
            var vsizeCredentialTask  = round.VsizeCredentialIssuer.HandleRequestAsync(request.ZeroVsizeCredentialRequests, cancellationToken);

            if (round.RemainingInputVsizeAllocation < round.MaxVsizeAllocationPerAlice)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.VsizeQuotaExceeded);
            }

            var commitAmountCredentialResponse = await amountCredentialTask.ConfigureAwait(false);

            var commitVsizeCredentialResponse = await vsizeCredentialTask.ConfigureAwait(false);

            alice.SetDeadlineRelativeTo(round.ConnectionConfirmationTimeFrame.Duration);
            round.Alices.Add(alice);

            return(new(alice.Id,
                       commitAmountCredentialResponse,
                       commitVsizeCredentialResponse,
                       alice.IsPayingZeroCoordinationFee));
        }
    }
コード例 #16
0
 static void Main(string[] args)
 {
     Alice.Create().PublicSendMessageToBob();
     Bob.Create().PublicSendMessageToAlice();
 }
コード例 #17
0
        public async Task <IActionResult> PostInputsAsync([FromBody, Required] InputsRequest4 request)
        {
            // Validate request.
            if (request.RoundId < 0)
            {
                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
                {
                    var 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.Select(x => x.BlindedOutput)))
                    {
                        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;
                        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, 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."));
                            }
                        }

                        OutPoint outpoint   = inputProof.Input;
                        var      bannedElem = await Coordinator.UtxoReferee.TryGetBannedAsync(outpoint, notedToo : false);

                        if (bannedElem is { })
                        {
                            return(BadRequest($"Input is banned from participation for {(int)bannedElem.BannedRemaining.TotalMinutes} minutes: {inputProof.Input.N}:{inputProof.Input.Hash}."));
                        }

                        var txOutResponseTask = batch.GetTxOutAsync(inputProof.Input.Hash, (int)inputProof.Input.N, includeMempool: true);
                        getTxOutResponses.Add((inputProof, txOutResponseTask));
                    }

                    // Perform all RPC request at once
                    await batch.SendBatchAsync();

                    byte[]  blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputs.Select(x => x.BlindedOutput.ToBytes()));
                    uint256 blindedOutputScriptsHash      = new uint256(Hashes.SHA256(blindedOutputScriptHashesByte));

                    var inputs = new HashSet <Coin>();

                    var allInputsConfirmed = true;
                    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.N}:{inputProof.Input.Hash}."));
                        }

                        // Check if unconfirmed.
                        if (getTxOutResponse.Confirmations <= 0)
                        {
                            return(BadRequest("Provided input is unconfirmed."));
                        }

                        // Check if immature.
                        if (getTxOutResponse.IsCoinBase && getTxOutResponse.Confirmations <= 100)
                        {
                            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, txOut));
                    }

                    if (!allInputsConfirmed)
                    {
                        // Check if mempool would accept a fake transaction created with the registered inputs.
                        // Fake outputs: mixlevels + 1 maximum, +1 because there can be a change.
                        var result = await RpcClient.TestMempoolAcceptAsync(inputs, fakeOutputCount : round.MixingLevels.Count() + 1, round.FeePerInputs, round.FeePerOutputs);

                        if (!result.accept)
                        {
                            return(BadRequest($"Provided input is from an unconfirmed coinjoin, but a limit is reached: {result.rejectReason}"));
                        }
                    }

                    var acceptedBlindedOutputScripts = new List <BlindedOutputWithNonceIndex>();

                    // 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.Select(x => x.BlindedOutput));

                    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.BlindedOutput, round.NonceProvider.GetNonceKeyForIndex(blindedOutput.N));
                        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));
                }
コード例 #18
0
 public void Ban(Alice alice, uint256 lastDisruptedRoundId, bool isLongBan = false)
 {
     Punish(alice.Coin.Outpoint, Punishment.Banned, lastDisruptedRoundId, isLongBan);
 }