/// <inheritdoc />
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var value = (string)reader.Value;
        var op    = new OutPoint();

        op.FromHex(value);
        return(op);
    }
예제 #2
0
        public void CanEncodeDecodeOutpoint()
        {
            var    outPoint = new OutPoint(new Transaction().GetHash(), 0);
            string encoded  = outPoint.ToHex();
            var    decoded  = new OutPoint();

            decoded.FromHex(encoded);
            Assert.Equal(outPoint, decoded);
        }
예제 #3
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
                }));
            }
        }