예제 #1
0
        public IActionResult CoinJoin([FromBody] CoinJoinRequest request)
        {
            var          roundId = Global.StateMachine.RoundId;
            TumblerPhase phase   = TumblerPhase.Signing;

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

                Global.StateMachine.FindAlice(request.UniqueId, throwException: true);
                AssertPhase(roundId, phase);

                return(new ObjectResult(new CoinJoinResponse
                {
                    Transaction = Global.StateMachine.UnsignedCoinJoinHex
                }));
            }
            catch (Exception ex)
            {
                return(new ObjectResult(new FailureResponse {
                    Message = ex.Message
                }));
            }
        }
예제 #2
0
 private static void AssertPhase(int roundId, TumblerPhase phase)
 {
     if (Global.StateMachine.Phase != phase || Global.StateMachine.RoundId != roundId)
     {
         throw new InvalidOperationException("Phase timed out");
     }
 }
예제 #3
0
 public TumblerStateMachine()
 {
     Phase         = TumblerPhase.InputRegistration;
     RoundId       = 0;
     AcceptRequest = false;
     FallBackRound = false;
     CoinJoin      = null;
     InputRegistrationStopwatch = new Stopwatch();
 }
예제 #4
0
        public void UpdatePhase(TumblerPhase phase)
        {
            if (phase == Phase)
            {
                return;
            }
            AcceptRequest = false;

            Phase = phase;
            _ctsPhaseCancel.Cancel();
            _ctsPhaseCancel = new CancellationTokenSource();
        }
예제 #5
0
        public IActionResult ConnectionConfirmation([FromBody] ConnectionConfirmationRequest request)
        {
            var          roundId = Global.StateMachine.RoundId;
            TumblerPhase phase   = TumblerPhase.ConnectionConfirmation;

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

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

                if (alice.State == AliceState.ConnectionConfirmed)
                {
                    return(new ObjectResult(new ConnectionConfirmationResponse {
                        RoundHash = Global.StateMachine.RoundHash
                    }));
                }

                AssertPhase(roundId, phase);
                alice.State = AliceState.ConnectionConfirmed;

                try
                {
                    return(new ObjectResult(new ConnectionConfirmationResponse {
                        RoundHash = Global.StateMachine.RoundHash
                    }));
                }
                finally
                {
                    if (Global.StateMachine.Alices.All(x => x.State == AliceState.ConnectionConfirmed))
                    {
                        Global.StateMachine.UpdatePhase(TumblerPhase.OutputRegistration);
                    }
                }
            }
            catch (Exception ex)
            {
                return(new ObjectResult(new FailureResponse {
                    Message = ex.Message
                }));
            }
        }
예제 #6
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
                }));
            }
        }
예제 #7
0
        public IActionResult Output([FromBody] OutputRequest request)
        {
            var          roundId = Global.StateMachine.RoundId;
            TumblerPhase phase   = TumblerPhase.OutputRegistration;

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

                if (string.IsNullOrWhiteSpace(request.Output))
                {
                    return(new BadRequestResult());
                }
                if (string.IsNullOrWhiteSpace(request.Signature))
                {
                    return(new BadRequestResult());
                }
                if (string.IsNullOrWhiteSpace(request.RoundHash))
                {
                    return(new BadRequestResult());
                }

                if (request.RoundHash != Global.StateMachine.RoundHash)
                {
                    throw new ArgumentException("Wrong round hash provided");
                }

                var output = new BitcoinWitPubKeyAddress(request.Output, expectedNetwork: Global.Config.Network);
                // if not already registered
                if (Global.StateMachine.Bobs.Any(x => x.Output == output))
                {
                    return(new ObjectResult(new SuccessResponse()));
                }

                if (Global.RsaKey.PubKey.Verify(HexHelpers.GetBytes(request.Signature), Encoding.UTF8.GetBytes(request.Output)))
                {
                    try
                    {
                        AssertPhase(roundId, phase);

                        Global.StateMachine.Bobs.Add(new Bob {
                            Output = output
                        });

                        return(new ObjectResult(new SuccessResponse()));
                    }
                    finally
                    {
                        if (Global.StateMachine.Alices.Count == Global.StateMachine.Bobs.Count)
                        {
                            Global.StateMachine.UpdatePhase(TumblerPhase.Signing);
                        }
                    }
                }
                else
                {
                    throw new ArgumentException("Bad output");
                }
            }
            catch (Exception ex)
            {
                return(new ObjectResult(new FailureResponse {
                    Message = ex.Message
                }));
            }
        }