Ejemplo n.º 1
0
    public UtxoReferee(Network network, string folderPath, IRPCClient rpc, CoordinatorRoundConfig roundConfig)
    {
        Network     = Guard.NotNull(nameof(network), network);
        FolderPath  = Guard.NotNullOrEmptyOrWhitespace(nameof(folderPath), folderPath, trim: true);
        RpcClient   = Guard.NotNull(nameof(rpc), rpc);
        RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig);

        BannedUtxos = new ConcurrentDictionary <OutPoint, BannedUtxo>();

        Directory.CreateDirectory(FolderPath);

        if (File.Exists(BannedUtxosFilePath))
        {
            try
            {
                var      toRemove = new List <string>();           // what's been confirmed
                string[] allLines = File.ReadAllLines(BannedUtxosFilePath);
                foreach (string line in allLines)
                {
                    var bannedRecord = BannedUtxo.FromString(line);

                    var getTxOutResponse = RpcClient.GetTxOutAsync(bannedRecord.Utxo.Hash, (int)bannedRecord.Utxo.N, includeMempool: true).GetAwaiter().GetResult();

                    // Check if inputs are unspent.
                    if (getTxOutResponse is null)
                    {
                        toRemove.Add(line);
                    }
                    else
                    {
                        BannedUtxos.TryAdd(bannedRecord.Utxo, bannedRecord);
                    }
                }

                if (toRemove.Count != 0)                 // a little performance boost, often it'll be empty
                {
                    var newAllLines = allLines.Where(x => !toRemove.Contains(x));
                    File.WriteAllLines(BannedUtxosFilePath, newAllLines);
                }

                Logger.LogInfo($"{allLines.Length} banned UTXOs are loaded from {BannedUtxosFilePath}.");
            }
            catch (Exception ex)
            {
                Logger.LogWarning($"Banned UTXO file got corrupted. Deleting {BannedUtxosFilePath}. {ex.GetType()}: {ex.Message}");
                File.Delete(BannedUtxosFilePath);
            }
        }
        else
        {
            Logger.LogInfo($"No banned UTXOs are loaded from {BannedUtxosFilePath}.");
        }
    }
Ejemplo n.º 2
0
        public void UtxoRefereeSerialization()
        {
            var record = BannedUtxo.FromString("2018-11-23 15-23-14:1:44:2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492:True:195");

            Assert.Equal(new DateTimeOffset(2018, 11, 23, 15, 23, 14, TimeSpan.Zero), record.TimeOfBan);
            Assert.Equal(1, record.Severity);
            Assert.Equal(44u, record.Utxo.N);
            Assert.Equal(new uint256("2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492"), record.Utxo.Hash);
            Assert.True(record.IsNoted);
            Assert.Equal(195, record.BannedForRound);

            DateTimeOffset dateTime    = DateTimeOffset.UtcNow;
            DateTimeOffset now         = new(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), TimeSpan.Zero);
            var            record2Init = new BannedUtxo(record.Utxo, 3, now, false, 99);
            string         record2Line = record2Init.ToString();
            var            record2     = BannedUtxo.FromString(record2Line);

            Assert.Equal(now, record2.TimeOfBan);
            Assert.Equal(3, record2.Severity);
            Assert.Equal(44u, record2.Utxo.N);
            Assert.Equal(new uint256("2716e680f47d74c1bc6f031da22331564dd4c6641d7216576aad1b846c85d492"), record2.Utxo.Hash);
            Assert.False(record2.IsNoted);
            Assert.Equal(99, record2.BannedForRound);
        }
Ejemplo n.º 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
                }));
            }
        }