Exemplo n.º 1
0
        public void Update()
        {
            int             height = Services.BlockExplorerService.GetCurrentHeight();
            CycleParameters cycle;
            CyclePhase      phase;

            if (ClientChannelNegotiation == null)
            {
                cycle = Parameters.CycleGenerator.GetRegistratingCycle(height);
                phase = CyclePhase.Registration;
            }
            else
            {
                cycle = ClientChannelNegotiation.GetCycle();
                var phases = new CyclePhase[]
                {
                    CyclePhase.Registration,
                    CyclePhase.ClientChannelEstablishment,
                    CyclePhase.TumblerChannelEstablishment,
                    CyclePhase.PaymentPhase,
                    CyclePhase.TumblerCashoutPhase,
                    CyclePhase.ClientCashoutPhase
                };
                if (!phases.Any(p => cycle.IsInPhase(p, height)))
                {
                    return;
                }
                phase = phases.First(p => cycle.IsInPhase(p, height));
            }


            Logs.Client.LogInformation(Environment.NewLine);
            var period     = cycle.GetPeriods().GetPeriod(phase);
            var blocksLeft = period.End - height;

            Logs.Client.LogInformation($"Cycle {cycle.Start} ({Status})");
            Logs.Client.LogInformation($"{cycle.ToString(height)} in phase {phase} ({blocksLeft} more blocks)");
            var           previousState = Status;
            TumblerClient bob = null, alice = null;

            try
            {
                var correlation = SolverClientSession == null ? CorrelationId.Zero : new CorrelationId(SolverClientSession.Id);

                FeeRate feeRate = null;
                switch (phase)
                {
                case CyclePhase.Registration:
                    if (ClientChannelNegotiation == null)
                    {
                        bob = Runtime.CreateTumblerClient(cycle.Start, Identity.Bob);
                        //Client asks for voucher
                        var voucherResponse = bob.AskUnsignedVoucher();
                        //Client ensures he is in the same cycle as the tumbler (would fail if one tumbler or client's chain isn't sync)
                        var tumblerCycle = Parameters.CycleGenerator.GetCycle(voucherResponse.CycleStart);
                        Assert(tumblerCycle.Start == cycle.Start, "invalid-phase");
                        //Saving the voucher for later
                        StartCycle = cycle.Start;
                        ClientChannelNegotiation = new ClientChannelNegotiation(Parameters, cycle.Start);
                        ClientChannelNegotiation.ReceiveUnsignedVoucher(voucherResponse);
                        Status = PaymentStateMachineStatus.Registered;
                    }
                    break;

                case CyclePhase.ClientChannelEstablishment:
                    if (ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingTumblerClientTransactionKey)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Identity.Alice);
                        var key = alice.RequestTumblerEscrowKey();
                        ClientChannelNegotiation.ReceiveTumblerEscrowKey(key.PubKey, key.KeyIndex);
                        //Client create the escrow
                        var escrowTxOut = ClientChannelNegotiation.BuildClientEscrowTxOut();
                        feeRate = GetFeeRate();

                        Transaction clientEscrowTx = null;
                        try
                        {
                            clientEscrowTx = Services.WalletService.FundTransactionAsync(escrowTxOut, feeRate).GetAwaiter().GetResult();
                        }
                        catch (NotEnoughFundsException ex)
                        {
                            Logs.Client.LogInformation($"Not enough funds in the wallet to tumble. Missing about {ex.Missing}. Denomination is {Parameters.Denomination}.");
                            break;
                        }

                        var redeemDestination = Services.WalletService.GenerateAddress().ScriptPubKey;
                        var channelId         = new uint160(RandomUtils.GetBytes(20));
                        SolverClientSession = ClientChannelNegotiation.SetClientSignedTransaction(channelId, clientEscrowTx, redeemDestination);


                        correlation = new CorrelationId(SolverClientSession.Id);

                        Tracker.AddressCreated(cycle.Start, TransactionType.ClientEscrow, escrowTxOut.ScriptPubKey, correlation);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.ClientEscrow, clientEscrowTx.GetHash(), correlation);
                        Services.BlockExplorerService.TrackAsync(escrowTxOut.ScriptPubKey).GetAwaiter().GetResult();


                        var redeemTx = SolverClientSession.CreateRedeemTransaction(feeRate);
                        Tracker.AddressCreated(cycle.Start, TransactionType.ClientRedeem, redeemDestination, correlation);

                        //redeemTx does not be to be recorded to the tracker, this is TrustedBroadcastService job

                        Services.BroadcastService.BroadcastAsync(clientEscrowTx).GetAwaiter().GetResult();

                        Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.ClientRedeem, correlation, redeemTx);

                        Status = PaymentStateMachineStatus.ClientChannelBroadcasted;
                    }
                    else if (ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingSolvedVoucher)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Identity.Alice);
                        TransactionInformation clientTx = GetTransactionInformation(SolverClientSession.EscrowedCoin, true);
                        var state = ClientChannelNegotiation.GetInternalState();
                        if (clientTx != null && clientTx.Confirmations >= cycle.SafetyPeriodDuration)
                        {
                            Logs.Client.LogInformation($"Client escrow reached {cycle.SafetyPeriodDuration} confirmations");
                            //Client asks the public key of the Tumbler and sends its own
                            var voucher = alice.SignVoucher(new SignVoucherRequest
                            {
                                MerkleProof     = clientTx.MerkleProof,
                                Transaction     = clientTx.Transaction,
                                KeyReference    = state.TumblerEscrowKeyReference,
                                UnsignedVoucher = state.BlindedVoucher,
                                Cycle           = cycle.Start,
                                ClientEscrowKey = state.ClientEscrowKey.PubKey,
                                ChannelId       = SolverClientSession.Id
                            });
                            ClientChannelNegotiation.CheckVoucherSolution(voucher);
                            Status = PaymentStateMachineStatus.TumblerVoucherObtained;
                        }
                    }
                    break;

                case CyclePhase.TumblerChannelEstablishment:

                    bob = Runtime.CreateTumblerClient(cycle.Start, Identity.Bob);
                    if (Status == PaymentStateMachineStatus.TumblerVoucherObtained)
                    {
                        Logs.Client.LogInformation("Begin ask to open the channel...");
                        //Client asks the Tumbler to make a channel
                        var     bobEscrowInformation = ClientChannelNegotiation.GetOpenChannelRequest();
                        uint160 channelId            = null;
                        try
                        {
                            channelId = bob.BeginOpenChannel(bobEscrowInformation);
                        }
                        catch (Exception ex)
                        {
                            if (ex.Message.Contains("tumbler-insufficient-funds"))
                            {
                                Logs.Client.LogWarning("The tumbler server has not enough funds and can't open a channel for now");
                                break;
                            }
                            throw;
                        }


                        ClientChannelNegotiation.SetChannelId(channelId);
                        Status = PaymentStateMachineStatus.TumblerChannelCreating;
                    }
                    else if (Status == PaymentStateMachineStatus.TumblerChannelCreating)
                    {
                        var tumblerEscrow = bob.EndOpenChannel(cycle.Start, ClientChannelNegotiation.GetInternalState().ChannelId);
                        if (tumblerEscrow == null)
                        {
                            Logs.Client.LogInformation("Tumbler escrow still creating...");
                            break;
                        }

                        PromiseClientSession = ClientChannelNegotiation.ReceiveTumblerEscrowedCoin(tumblerEscrow);
                        Logs.Client.LogInformation("Tumbler escrow broadcasted");
                        //Tell to the block explorer we need to track that address (for checking if it is confirmed in payment phase)
                        Services.BlockExplorerService.TrackAsync(PromiseClientSession.EscrowedCoin.ScriptPubKey).GetAwaiter().GetResult();
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.ScriptPubKey, correlation);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.Outpoint.Hash, correlation);

                        //Channel is done, now need to run the promise protocol to get valid puzzle
                        var cashoutDestination = DestinationWallet.GetNewDestination();
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerCashout, cashoutDestination, correlation);

                        feeRate = GetFeeRate();
                        var sigReq      = PromiseClientSession.CreateSignatureRequest(cashoutDestination, feeRate);
                        var commitments = bob.SignHashes(PromiseClientSession.Id, sigReq);
                        var revelation  = PromiseClientSession.Reveal(commitments);
                        var proof       = bob.CheckRevelation(PromiseClientSession.Id, revelation);
                        var puzzle      = PromiseClientSession.CheckCommitmentProof(proof);
                        SolverClientSession.AcceptPuzzle(puzzle[0]);
                        Status = PaymentStateMachineStatus.TumblerChannelBroadcasted;
                    }
                    else if (Status == PaymentStateMachineStatus.TumblerChannelBroadcasted)
                    {
                        CheckTumblerChannelConfirmed(cycle);
                    }
                    break;

                case CyclePhase.PaymentPhase:
                    //Could have confirmed during safe period
                    //Only check for the first block when period start,
                    //else Tumbler can know deanonymize you based on the timing of first Alice request if the transaction was not confirmed previously
                    if (Status == PaymentStateMachineStatus.TumblerChannelBroadcasted && height == period.Start)
                    {
                        CheckTumblerChannelConfirmed(cycle);
                    }
                    if (PromiseClientSession != null && Status == PaymentStateMachineStatus.TumblerChannelConfirmed)
                    {
                        TransactionInformation tumblerTx = GetTransactionInformation(PromiseClientSession.EscrowedCoin, false);
                        //Ensure the tumbler coin is confirmed before paying anything

                        if (tumblerTx != null && tumblerTx.Confirmations >= cycle.SafetyPeriodDuration)
                        {
                            if (SolverClientSession.Status == SolverClientStates.WaitingGeneratePuzzles)
                            {
                                feeRate = GetFeeRate();
                                alice   = Runtime.CreateTumblerClient(cycle.Start, Identity.Alice);
                                var puzzles          = SolverClientSession.GeneratePuzzles();
                                var commmitments     = alice.SolvePuzzles(SolverClientSession.Id, puzzles);
                                var revelation2      = SolverClientSession.Reveal(commmitments);
                                var solutionKeys     = alice.CheckRevelation(SolverClientSession.Id, revelation2);
                                var blindFactors     = SolverClientSession.GetBlindFactors(solutionKeys);
                                var offerInformation = alice.CheckBlindFactors(SolverClientSession.Id, blindFactors);

                                var offerSignature = SolverClientSession.SignOffer(offerInformation);

                                var offerRedeem = SolverClientSession.CreateOfferRedeemTransaction(feeRate);
                                //May need to find solution in the fulfillment transaction
                                Services.BlockExplorerService.TrackAsync(offerRedeem.PreviousScriptPubKey).GetAwaiter().GetResult();
                                Tracker.AddressCreated(cycle.Start, TransactionType.ClientOfferRedeem, SolverClientSession.GetInternalState().RedeemDestination, correlation);
                                Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.ClientOfferRedeem, correlation, offerRedeem);
                                try
                                {
                                    solutionKeys = alice.FulfillOffer(SolverClientSession.Id, offerSignature);
                                    SolverClientSession.CheckSolutions(solutionKeys);
                                    var tumblingSolution = SolverClientSession.GetSolution();
                                    // TODO: Figure out which puzzle is passed here.
                                    var transaction = PromiseClientSession.GetSignedTransaction(tumblingSolution, 0);
                                    Logs.Client.LogInformation("Got puzzle solution cooperatively from the tumbler");
                                    Status = PaymentStateMachineStatus.PuzzleSolutionObtained;
                                    Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.TumblerCashout, correlation, new TrustedBroadcastRequest()
                                    {
                                        BroadcastAt = cycle.GetPeriods().ClientCashout.Start,
                                        Transaction = transaction
                                    });
                                    if (Cooperative)
                                    {
                                        var signature = SolverClientSession.SignEscape();
                                        // No need to await for it, it is a just nice for the tumbler (we don't want the underlying socks connection cut before the escape key is sent)
                                        alice.GiveEscapeKeyAsync(SolverClientSession.Id, signature).GetAwaiter().GetResult();
                                        Logs.Client.LogInformation("Gave escape signature to the tumbler");
                                    }
                                }
                                catch (Exception ex)
                                {
                                    Status = PaymentStateMachineStatus.UncooperativeTumbler;
                                    Logs.Client.LogWarning("The tumbler did not gave puzzle solution cooperatively");
                                    Logs.Client.LogWarning(ex.ToString());
                                }
                            }
                        }
                    }
                    break;

                case CyclePhase.ClientCashoutPhase:
                    if (SolverClientSession != null)
                    {
                        //If the tumbler is uncooperative, he published solutions on the blockchain
                        if (SolverClientSession.Status == SolverClientStates.WaitingPuzzleSolutions)
                        {
                            var transactions = Services.BlockExplorerService.GetTransactions(SolverClientSession.GetInternalState().OfferCoin.ScriptPubKey, false);
                            if (transactions.Count != 0)
                            {
                                SolverClientSession.CheckSolutions(transactions.Select(t => t.Transaction).ToArray());
                                Logs.Client.LogInformation("Puzzle solution recovered from tumbler's fulfill transaction");
                                Status = PaymentStateMachineStatus.PuzzleSolutionObtained;
                                var tumblingSolution = SolverClientSession.GetSolution();
                                // TODO: Figure out which puzzle is passed here.
                                var transaction = PromiseClientSession.GetSignedTransaction(tumblingSolution, 0);
                                Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerCashout, transaction.GetHash(), correlation);
                                Services.BroadcastService.BroadcastAsync(transaction).GetAwaiter().GetResult();
                            }
                        }
                    }
                    break;
                }
            }
            finally
            {
                if (previousState != Status)
                {
                    Logs.Client.LogInformation($"Status changed {previousState} => {Status}");
                }
                if (alice != null && bob != null)
                {
                    throw new InvalidOperationException("Bob and Alice have been both initialized, please report the bug to NTumbleBit developers");
                }
                if (alice != null)
                {
                    alice.Dispose();
                }
                if (bob != null)
                {
                    bob.Dispose();
                }
            }
        }
Exemplo n.º 2
0
        public void Update()
        {
            int             height = Services.BlockExplorerService.GetCurrentHeight();
            CycleParameters cycle;
            CyclePhase      phase;

            if (ClientChannelNegotiation == null)
            {
                cycle = Parameters.CycleGenerator.GetRegistratingCycle(height);
                phase = CyclePhase.Registration;
            }
            else
            {
                cycle = ClientChannelNegotiation.GetCycle();
                var phases = new CyclePhase[]
                {
                    CyclePhase.Registration,
                    CyclePhase.ClientChannelEstablishment,
                    CyclePhase.TumblerChannelEstablishment,
                    CyclePhase.PaymentPhase,
                    CyclePhase.TumblerCashoutPhase,
                    CyclePhase.ClientCashoutPhase
                };
                if (!phases.Any(p => cycle.IsInPhase(p, height)))
                {
                    return;
                }
                phase = phases.First(p => cycle.IsInPhase(p, height));
            }


            Logs.Client.LogInformation("[[[Updating cycle " + cycle.Start + "]]]");

            Logs.Client.LogInformation("Phase " + Enum.GetName(typeof(CyclePhase), phase) + ", ending in " + (cycle.GetPeriods().GetPeriod(phase).End - height) + " blocks");

            TumblerClient bob = null, alice = null;

            try
            {
                var correlation = SolverClientSession == null ? 0 : GetCorrelation(SolverClientSession.EscrowedCoin);

                FeeRate feeRate = null;
                switch (phase)
                {
                case CyclePhase.Registration:
                    if (ClientChannelNegotiation == null)
                    {
                        bob = Runtime.CreateTumblerClient(cycle.Start, Identity.Bob);
                        //Client asks for voucher
                        var voucherResponse = bob.AskUnsignedVoucher();
                        //Client ensures he is in the same cycle as the tumbler (would fail if one tumbler or client's chain isn't sync)
                        var tumblerCycle = Parameters.CycleGenerator.GetCycle(voucherResponse.CycleStart);
                        Assert(tumblerCycle.Start == cycle.Start, "invalid-phase");
                        //Saving the voucher for later
                        StartCycle = cycle.Start;
                        ClientChannelNegotiation = new ClientChannelNegotiation(Parameters, cycle.Start);
                        ClientChannelNegotiation.ReceiveUnsignedVoucher(voucherResponse);
                        Logs.Client.LogInformation("Registered");
                    }
                    break;

                case CyclePhase.ClientChannelEstablishment:
                    if (ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingTumblerClientTransactionKey)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Identity.Alice);
                        var key = alice.RequestTumblerEscrowKey();
                        ClientChannelNegotiation.ReceiveTumblerEscrowKey(key.PubKey, key.KeyIndex);
                        //Client create the escrow
                        var escrowTxOut = ClientChannelNegotiation.BuildClientEscrowTxOut();
                        feeRate = GetFeeRate();

                        Transaction clientEscrowTx = null;
                        try
                        {
                            clientEscrowTx = Services.WalletService.FundTransaction(escrowTxOut, feeRate);
                        }
                        catch (NotEnoughFundsException ex)
                        {
                            Logs.Client.LogInformation($"Not enough funds in the wallet to tumble. Missing about {ex.Missing}. Denomination is {Parameters.Denomination}.");
                            break;
                        }

                        var redeemDestination = Services.WalletService.GenerateAddress().ScriptPubKey;
                        SolverClientSession = ClientChannelNegotiation.SetClientSignedTransaction(clientEscrowTx, redeemDestination);


                        correlation = GetCorrelation(SolverClientSession.EscrowedCoin);

                        Tracker.AddressCreated(cycle.Start, TransactionType.ClientEscrow, escrowTxOut.ScriptPubKey, correlation);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.ClientEscrow, clientEscrowTx.GetHash(), correlation);
                        Services.BlockExplorerService.Track(escrowTxOut.ScriptPubKey);


                        var redeemTx = SolverClientSession.CreateRedeemTransaction(feeRate);
                        Tracker.AddressCreated(cycle.Start, TransactionType.ClientRedeem, redeemDestination, correlation);

                        //redeemTx does not be to be recorded to the tracker, this is TrustedBroadcastService job

                        Services.BroadcastService.Broadcast(clientEscrowTx);

                        Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.ClientRedeem, correlation, redeemTx);

                        Logs.Client.LogInformation("Client channel broadcasted");
                    }
                    else if (ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingSolvedVoucher)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Identity.Alice);
                        TransactionInformation clientTx = GetTransactionInformation(SolverClientSession.EscrowedCoin, true);
                        var state = ClientChannelNegotiation.GetInternalState();
                        if (clientTx != null && clientTx.Confirmations >= cycle.SafetyPeriodDuration)
                        {
                            Logs.Client.LogInformation($"Client escrow reached {cycle.SafetyPeriodDuration} confirmations");
                            //Client asks the public key of the Tumbler and sends its own
                            var voucher = alice.SignVoucher(new SignVoucherRequest
                            {
                                MerkleProof     = clientTx.MerkleProof,
                                Transaction     = clientTx.Transaction,
                                KeyReference    = state.TumblerEscrowKeyReference,
                                UnsignedVoucher = state.BlindedVoucher,
                                Cycle           = cycle.Start,
                                ClientEscrowKey = state.ClientEscrowKey.PubKey
                            });
                            ClientChannelNegotiation.CheckVoucherSolution(voucher);
                            Logs.Client.LogInformation($"Tumbler escrow voucher obtained");
                        }
                    }
                    break;

                case CyclePhase.TumblerChannelEstablishment:
                    if (ClientChannelNegotiation != null && ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingGenerateTumblerTransactionKey)
                    {
                        bob = Runtime.CreateTumblerClient(cycle.Start, Identity.Bob);
                        //Client asks the Tumbler to make a channel
                        var bobEscrowInformation = ClientChannelNegotiation.GetOpenChannelRequest();
                        var tumblerInformation   = bob.OpenChannel(bobEscrowInformation);
                        PromiseClientSession = ClientChannelNegotiation.ReceiveTumblerEscrowedCoin(tumblerInformation);
                        Logs.Client.LogInformation("Tumbler escrow broadcasted");
                        //Tell to the block explorer we need to track that address (for checking if it is confirmed in payment phase)
                        Services.BlockExplorerService.Track(PromiseClientSession.EscrowedCoin.ScriptPubKey);
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.ScriptPubKey, correlation);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.Outpoint.Hash, correlation);

                        //Channel is done, now need to run the promise protocol to get valid puzzle
                        var cashoutDestination = DestinationWallet.GetNewDestination();
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerCashout, cashoutDestination, correlation);

                        feeRate = GetFeeRate();
                        var sigReq     = PromiseClientSession.CreateSignatureRequest(cashoutDestination, feeRate);
                        var commiments = bob.SignHashes(PromiseClientSession.Id, sigReq);
                        var revelation = PromiseClientSession.Reveal(commiments);
                        var proof      = bob.CheckRevelation(PromiseClientSession.Id, revelation);
                        var puzzle     = PromiseClientSession.CheckCommitmentProof(proof);
                        SolverClientSession.AcceptPuzzle(puzzle);
                        Logs.Client.LogInformation("Tumbler escrow puzzle obtained");
                    }
                    break;

                case CyclePhase.PaymentPhase:
                    if (PromiseClientSession != null)
                    {
                        TransactionInformation tumblerTx = GetTransactionInformation(PromiseClientSession.EscrowedCoin, false);
                        //Ensure the tumbler coin is confirmed before paying anything
                        if (tumblerTx != null || tumblerTx.Confirmations >= cycle.SafetyPeriodDuration)
                        {
                            Logs.Client.LogInformation($"Client escrow reached {cycle.SafetyPeriodDuration} confirmations");

                            if (SolverClientSession.Status == SolverClientStates.WaitingGeneratePuzzles)
                            {
                                feeRate = GetFeeRate();
                                alice   = Runtime.CreateTumblerClient(cycle.Start, Identity.Alice);
                                var puzzles          = SolverClientSession.GeneratePuzzles();
                                var commmitments     = alice.SolvePuzzles(SolverClientSession.Id, puzzles);
                                var revelation2      = SolverClientSession.Reveal(commmitments);
                                var solutionKeys     = alice.CheckRevelation(SolverClientSession.Id, revelation2);
                                var blindFactors     = SolverClientSession.GetBlindFactors(solutionKeys);
                                var offerInformation = alice.CheckBlindFactors(SolverClientSession.Id, blindFactors);

                                var offerSignature = SolverClientSession.SignOffer(offerInformation);

                                var offerRedeem = SolverClientSession.CreateOfferRedeemTransaction(feeRate);
                                //May need to find solution in the fulfillment transaction
                                Services.BlockExplorerService.Track(offerRedeem.PreviousScriptPubKey);
                                Tracker.AddressCreated(cycle.Start, TransactionType.ClientOfferRedeem, SolverClientSession.GetInternalState().RedeemDestination, correlation);
                                Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.ClientOfferRedeem, correlation, offerRedeem);
                                try
                                {
                                    solutionKeys = alice.FulfillOffer(SolverClientSession.Id, offerSignature);
                                    SolverClientSession.CheckSolutions(solutionKeys);
                                    var tumblingSolution = SolverClientSession.GetSolution();
                                    var transaction      = PromiseClientSession.GetSignedTransaction(tumblingSolution);
                                    Logs.Client.LogInformation("Got puzzle solution cooperatively from the tumbler");
                                    Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.TumblerCashout, correlation, new TrustedBroadcastRequest()
                                    {
                                        BroadcastAt = cycle.GetPeriods().ClientCashout.Start,
                                        Transaction = transaction
                                    });
                                    if (Cooperative)
                                    {
                                        var signature = SolverClientSession.SignEscape();
                                        alice.GiveEscapeKey(SolverClientSession.Id, signature);
                                        Logs.Client.LogInformation("Gave escape signature to the tumbler");
                                    }
                                }
                                catch (Exception ex)
                                {
                                    Logs.Client.LogWarning("The tumbler did not gave puzzle solution cooperatively");
                                    Logs.Client.LogWarning(ex.ToString());
                                }
                            }
                        }
                    }
                    break;

                case CyclePhase.ClientCashoutPhase:
                    if (SolverClientSession != null)
                    {
                        //If the tumbler is uncooperative, he published solutions on the blockchain
                        if (SolverClientSession.Status == SolverClientStates.WaitingPuzzleSolutions)
                        {
                            var transactions = Services.BlockExplorerService.GetTransactions(SolverClientSession.GetInternalState().OfferCoin.ScriptPubKey, false);
                            if (transactions.Length != 0)
                            {
                                SolverClientSession.CheckSolutions(transactions.Select(t => t.Transaction).ToArray());
                                Logs.Client.LogInformation("Puzzle solution recovered from tumbler's fulfill transaction");

                                var tumblingSolution = SolverClientSession.GetSolution();
                                var transaction      = PromiseClientSession.GetSignedTransaction(tumblingSolution);
                                Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerCashout, transaction.GetHash(), correlation);
                                Services.BroadcastService.Broadcast(transaction);
                            }
                        }
                    }
                    break;
                }
            }
            finally
            {
                if (alice != null && bob != null)
                {
                    throw new InvalidOperationException("Bob and Alice have been both initialized, please report the bug to NTumbleBit developers");
                }
                if (alice != null)
                {
                    alice.Dispose();
                }
                if (bob != null)
                {
                    bob.Dispose();
                }
            }
        }
Exemplo n.º 3
0
        public CycleProgressInfo Update()
        {
            int             height = Services.BlockExplorerService.GetCurrentHeight();
            CycleParameters cycle;
            CyclePhase      phase;
            CyclePeriod     period;
            bool            isSafetyPeriod = false;

            if (ClientChannelNegotiation == null)
            {
                cycle  = Parameters.CycleGenerator.GetRegisteringCycle(height);
                phase  = CyclePhase.Registration;
                period = cycle.GetPeriods().GetPeriod(phase);
            }
            else
            {
                cycle = ClientChannelNegotiation.GetCycle();
                var phases = new CyclePhase[]
                {
                    CyclePhase.Registration,
                    CyclePhase.ClientChannelEstablishment,
                    CyclePhase.TumblerChannelEstablishment,
                    CyclePhase.PaymentPhase,
                    CyclePhase.TumblerCashoutPhase,
                    CyclePhase.ClientCashoutPhase
                };

                if (cycle.IsComplete(height))
                {
                    return(null);
                }

                //If we are not in any phase we are in the SaftyPeriod.
                if (!phases.Any(p => cycle.IsInPhase(p, height)))
                {
                    //Find last CyclePhase
                    phase = CyclePhase.Registration;
                    for (int i = height - 1; i >= cycle.Start; i--)
                    {
                        if (phases.Any(p => cycle.IsInPhase(p, i)))
                        {
                            phase = phases.First(p => cycle.IsInPhase(p, i));
                            break;
                        }
                    }

                    period         = cycle.GetPeriods().GetPeriod(phase);
                    period.End    += cycle.SafetyPeriodDuration;
                    isSafetyPeriod = true;
                }
                else
                {
                    phase          = phases.First(p => cycle.IsInPhase(p, height));
                    period         = cycle.GetPeriods().GetPeriod(phase);
                    isSafetyPeriod = false;
                }
            }

            var blocksLeft = period.End - height;

            Logs.Client.LogInformation($"Cycle {cycle.Start} ({Status})");
            Logs.Client.LogInformation($"{cycle.ToString(height)} in phase {phase} ({blocksLeft} more blocks)");
            var previousState = Status;

            var progressInfo = new CycleProgressInfo(period, height, blocksLeft, cycle.Start, Status, phase, isSafetyPeriod, $"{cycle.ToString(height)} in phase {phase} ({blocksLeft} more blocks)");

            TumblerClient bob = null, alice = null;

            try
            {
                var correlation = SolverClientSession == null ? CorrelationId.Zero : new CorrelationId(SolverClientSession.Id);

                FeeRate feeRate = null;
                switch (phase)
                {
                case CyclePhase.Registration:
                    if (Status == PaymentStateMachineStatus.New)
                    {
                        bob = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Bob);
                        //Client asks for voucher
                        var voucherResponse = bob.AskUnsignedVoucher();
                        NeedSave = true;
                        //Client ensures he is in the same cycle as the tumbler (would fail if one tumbler or client's chain isn't sync)
                        var tumblerCycle = Parameters.CycleGenerator.GetCycle(voucherResponse.CycleStart);
                        Assert(tumblerCycle.Start == cycle.Start, "invalid-phase");
                        //Saving the voucher for later
                        StartCycle = cycle.Start;
                        ClientChannelNegotiation = new ClientChannelNegotiation(Parameters, cycle.Start);
                        ClientChannelNegotiation.ReceiveUnsignedVoucher(voucherResponse);
                        Status = PaymentStateMachineStatus.Registered;
                    }
                    break;

                case CyclePhase.ClientChannelEstablishment:
                    if (Status == PaymentStateMachineStatus.Registered)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Alice);
                        var key = alice.RequestTumblerEscrowKey();
                        ClientChannelNegotiation.ReceiveTumblerEscrowKey(key.PubKey, key.KeyIndex);
                        //Client create the escrow
                        var escrowTxOut = ClientChannelNegotiation.BuildClientEscrowTxOut();

                        Logs.Client.LogDebug($"Alice ClientChannelEstablishment escrowTxOut.ScriptPubKey: {escrowTxOut.ScriptPubKey.ToHex()}, value: {escrowTxOut.Value})");

                        feeRate = GetFeeRate();

                        Logs.Client.LogDebug($"Alice ClientChannelEstablishment feeRate.FeePerK: {feeRate.FeePerK})");

                        Transaction clientEscrowTx = null;
                        try
                        {
                            clientEscrowTx = Services.WalletService.FundTransactionAsync(escrowTxOut, feeRate).GetAwaiter().GetResult();
                        }
                        catch (NotEnoughFundsException ex)
                        {
                            Logs.Client.LogInformation($"Not enough funds in the wallet to tumble. Missing about {ex.Missing}. Denomination is {Parameters.Denomination}.");
                            break;
                        }

                        Logs.Client.LogDebug($"Alice ClientChannelEstablishment clientEscrowTx: {clientEscrowTx})");

                        NeedSave = true;
                        var redeemDestination = Services.WalletService.GenerateAddressAsync().GetAwaiter().GetResult().ScriptPubKey;
                        var channelId         = new uint160(RandomUtils.GetBytes(20));
                        SolverClientSession = ClientChannelNegotiation.SetClientSignedTransaction(channelId, clientEscrowTx, redeemDestination);

                        correlation = new CorrelationId(SolverClientSession.Id);

                        Tracker.AddressCreated(cycle.Start, TransactionType.ClientEscrow, escrowTxOut.ScriptPubKey, correlation);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.ClientEscrow, clientEscrowTx.GetHash(), correlation);
                        Services.BlockExplorerService.TrackAsync(escrowTxOut.ScriptPubKey).GetAwaiter().GetResult();

                        var redeemTx = SolverClientSession.CreateRedeemTransaction(feeRate);

                        Logs.Client.LogDebug($"Alice ClientChannelEstablishment redeemTx.Transaction: {redeemTx.Transaction}, redeemTx.BroadcastableHeight: {redeemTx.BroadcastableHeight})");

                        Tracker.AddressCreated(cycle.Start, TransactionType.ClientRedeem, redeemDestination, correlation);

                        //redeemTx does not be to be recorded to the tracker, this is TrustedBroadcastService job

                        Services.BroadcastService.BroadcastAsync(clientEscrowTx).GetAwaiter().GetResult();

                        Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.ClientRedeem, correlation, redeemTx);
                        Status = PaymentStateMachineStatus.ClientChannelBroadcasted;
                    }
                    else if (Status == PaymentStateMachineStatus.ClientChannelBroadcasted)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Alice);
                        TransactionInformation clientTx = GetTransactionInformation(SolverClientSession.EscrowedCoin, true);
                        var state = ClientChannelNegotiation.GetInternalState();
                        if (clientTx != null && clientTx.Confirmations >= cycle.SafetyPeriodDuration)
                        {
                            Logs.Client.LogInformation($"Client escrow reached {cycle.SafetyPeriodDuration} confirmations");
                            //Client asks the public key of the Tumbler and sends its own
                            alice.BeginSignVoucher(new SignVoucherRequest
                            {
                                MerkleProof     = clientTx.MerkleProof,
                                Transaction     = clientTx.Transaction,
                                KeyReference    = state.TumblerEscrowKeyReference,
                                UnsignedVoucher = state.BlindedVoucher,
                                Cycle           = cycle.Start,
                                ClientEscrowKey = state.ClientEscrowKey.PubKey,
                                ChannelId       = SolverClientSession.Id
                            });
                            NeedSave = true;
                            Status   = PaymentStateMachineStatus.TumblerVoucherSigning;
                        }
                    }
                    else if (Status == PaymentStateMachineStatus.TumblerVoucherSigning)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Alice);
                        var voucher = alice.EndSignVoucher(SolverClientSession.Id);
                        if (voucher != null)
                        {
                            ClientChannelNegotiation.CheckVoucherSolution(voucher);
                            NeedSave = true;
                            Status   = PaymentStateMachineStatus.TumblerVoucherObtained;
                        }
                    }
                    break;

                case CyclePhase.TumblerChannelEstablishment:

                    if (Status == PaymentStateMachineStatus.TumblerVoucherObtained)
                    {
                        bob = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Bob);
                        Logs.Client.LogInformation("Begin ask to open the channel...");
                        //Client asks the Tumbler to make a channel
                        var     bobEscrowInformation = ClientChannelNegotiation.GetOpenChannelRequest();
                        uint160 channelId            = null;
                        try
                        {
                            channelId = bob.BeginOpenChannel(bobEscrowInformation);
                            NeedSave  = true;
                        }
                        catch (Exception ex)
                        {
                            if (ex.Message.Contains("tumbler-insufficient-funds"))
                            {
                                Logs.Client.LogWarning("The tumbler server has not enough funds and can't open a channel for now");
                                break;
                            }
                            throw;
                        }
                        ClientChannelNegotiation.SetChannelId(channelId);
                        Status = PaymentStateMachineStatus.TumblerChannelCreating;
                    }
                    else if (Status == PaymentStateMachineStatus.TumblerChannelCreating)
                    {
                        bob = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Bob);
                        var tumblerEscrow = bob.EndOpenChannel(cycle.Start, ClientChannelNegotiation.GetInternalState().ChannelId);
                        if (tumblerEscrow == null)
                        {
                            Logs.Client.LogInformation("Tumbler escrow still creating...");
                            break;
                        }
                        NeedSave = true;

                        if (tumblerEscrow.OutputIndex >= tumblerEscrow.Transaction.Outputs.Count)
                        {
                            Logs.Client.LogError("Tumbler escrow output out-of-bound");
                            Status = PaymentStateMachineStatus.Wasted;
                            break;
                        }

                        var txOut      = tumblerEscrow.Transaction.Outputs[tumblerEscrow.OutputIndex];
                        var outpoint   = new OutPoint(tumblerEscrow.Transaction.GetHash(), tumblerEscrow.OutputIndex);
                        var escrowCoin = new Coin(outpoint, txOut).ToScriptCoin(ClientChannelNegotiation.GetTumblerEscrowParameters(tumblerEscrow.EscrowInitiatorKey).ToScript());

                        Logs.Client.LogDebug($"tumblerEscrow.Transaction: {tumblerEscrow.Transaction}");
                        Logs.Client.LogDebug($"TumblerEscrow hex: {tumblerEscrow.Transaction.ToHex()}");

                        PromiseClientSession = ClientChannelNegotiation.ReceiveTumblerEscrowedCoin(escrowCoin);

                        Logs.Client.LogInformation("Tumbler expected escrowed coin received");
                        //Tell to the block explorer we need to track that address (for checking if it is confirmed in payment phase)
                        Services.BlockExplorerService.TrackAsync(PromiseClientSession.EscrowedCoin.ScriptPubKey).GetAwaiter().GetResult();
                        Services.BlockExplorerService.TrackPrunedTransactionAsync(tumblerEscrow.Transaction, tumblerEscrow.MerkleProof).GetAwaiter().GetResult();

                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.ScriptPubKey, correlation);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.Outpoint.Hash, correlation);

                        Services.BroadcastService.BroadcastAsync(tumblerEscrow.Transaction).GetAwaiter().GetResult();
                        //Channel is done, now need to run the promise protocol to get valid puzzle
                        var cashoutDestination = DestinationWallet.GetNewDestination();
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerCashout, cashoutDestination, correlation);

                        feeRate = GetFeeRate();
                        var sigReq      = PromiseClientSession.CreateSignatureRequest(cashoutDestination, feeRate);
                        var commitments = bob.SignHashes(PromiseClientSession.Id, sigReq);
                        var revelation  = PromiseClientSession.Reveal(commitments);
                        var proof       = bob.CheckRevelation(PromiseClientSession.Id, revelation);
                        var puzzle      = PromiseClientSession.CheckCommitmentProof(proof);
                        SolverClientSession.AcceptPuzzle(puzzle);
                        Status = PaymentStateMachineStatus.TumblerChannelCreated;
                    }
                    else if (Status == PaymentStateMachineStatus.TumblerChannelCreated)
                    {
                        CheckTumblerChannelSecured(cycle);
                    }
                    break;

                case CyclePhase.PaymentPhase:
                    //Could have confirmed during safe period
                    //Only check for the first block when period start,
                    //else Tumbler can know deanonymize you based on the timing of first Alice request if the transaction was not confirmed previously
                    if (Status == PaymentStateMachineStatus.TumblerChannelCreated && height == period.Start)
                    {
                        CheckTumblerChannelSecured(cycle);
                    }
                    //No "else if" intended
                    if (Status == PaymentStateMachineStatus.TumblerChannelSecured)
                    {
                        alice = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Alice);
                        Logs.Client.LogDebug("Starting the puzzle solver protocol...");
                        var puzzles = SolverClientSession.GeneratePuzzles();
                        alice.BeginSolvePuzzles(SolverClientSession.Id, puzzles);
                        NeedSave = true;
                        Status   = PaymentStateMachineStatus.ProcessingPayment;
                    }
                    else if (Status == PaymentStateMachineStatus.ProcessingPayment)
                    {
                        feeRate = GetFeeRate();
                        alice   = Runtime.CreateTumblerClient(cycle.Start, Runtime.TumblerProtocol, Identity.Alice);
                        var commitments = alice.EndSolvePuzzles(SolverClientSession.Id);
                        NeedSave = true;
                        if (commitments == null)
                        {
                            Logs.Client.LogDebug("Still solving puzzles...");
                            break;
                        }

                        bool isRegtest       = Runtime.Network.Name == Network.RegTest.Name;
                        int  connectionCount = Services.BlockExplorerService.GetConnectionCount();
                        if (connectionCount < 4 && !isRegtest)
                        {
                            Logs.Client.LogWarning("There are too few bitcoin peers connected; payment will not be processed");
                            break;
                        }

                        var revelation2      = SolverClientSession.Reveal(commitments);
                        var solutionKeys     = alice.CheckRevelation(SolverClientSession.Id, revelation2);
                        var blindFactors     = SolverClientSession.GetBlindFactors(solutionKeys);
                        var offerInformation = alice.CheckBlindFactors(SolverClientSession.Id, blindFactors);

                        var offerSignature = SolverClientSession.SignOffer(offerInformation);

                        var offerRedeem = SolverClientSession.CreateOfferRedeemTransaction(feeRate);
                        Logs.Client.LogDebug("Puzzle solver protocol ended...");

                        Logs.Client.LogDebug($"offerRedeem.Transaction: {offerRedeem.Transaction} at height {offerRedeem.BroadcastableHeight}");

                        //May need to find solution in the fulfillment transaction
                        Services.BlockExplorerService.TrackAsync(offerRedeem.PreviousScriptPubKey).GetAwaiter().GetResult();
                        Tracker.AddressCreated(cycle.Start, TransactionType.ClientOfferRedeem, SolverClientSession.GetInternalState().RedeemDestination, correlation);
                        Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.ClientOfferRedeem, correlation, offerRedeem);
                        try
                        {
                            solutionKeys = alice.FulfillOffer(SolverClientSession.Id, offerSignature);
                            SolverClientSession.CheckSolutions(solutionKeys);
                            var tumblingSolution = SolverClientSession.GetSolution();
                            var transaction      = PromiseClientSession.GetSignedTransaction(tumblingSolution);
                            Logs.Client.LogDebug("Got puzzle solution cooperatively from the tumbler");

                            Logs.Client.LogDebug($"TumblerCashOut 1: {transaction}");
                            Logs.Client.LogDebug($"TumblerCashOut hex 1: {transaction.ToHex()}");

                            Status = PaymentStateMachineStatus.PuzzleSolutionObtained;
                            Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.TumblerCashout, correlation, new TrustedBroadcastRequest()
                            {
                                BroadcastAt = cycle.GetPeriods().ClientCashout.Start,
                                Transaction = transaction
                            });
                            if (Cooperative)
                            {
                                try
                                {
                                    // No need to await for it, it is a just nice for the tumbler (we don't want the underlying socks connection cut before the escape key is sent)
                                    var signature = SolverClientSession.SignEscape();
                                    alice.GiveEscapeKeyAsync(SolverClientSession.Id, signature).GetAwaiter().GetResult();
                                }
                                catch (Exception ex) { Logs.Client.LogDebug(new EventId(), ex, "Exception while giving the escape key"); }
                                Logs.Client.LogInformation("Gave escape signature to the tumbler");
                            }
                        }
                        catch (Exception ex)
                        {
                            Status = PaymentStateMachineStatus.UncooperativeTumbler;
                            Logs.Client.LogWarning("The tumbler did not gave puzzle solution cooperatively");
                            Logs.Client.LogWarning(ex.ToString());
                        }
                    }

                    break;

                case CyclePhase.ClientCashoutPhase:

                    //If the tumbler is uncooperative, he published solutions on the blockchain
                    if (Status == PaymentStateMachineStatus.UncooperativeTumbler)
                    {
                        var transactions = Services.BlockExplorerService.GetTransactionsAsync(SolverClientSession.GetInternalState().OfferCoin.ScriptPubKey, false).GetAwaiter().GetResult();
                        if (transactions.Count != 0)
                        {
                            SolverClientSession.CheckSolutions(transactions.Select(t => t.Transaction).ToArray());
                            Logs.Client.LogInformation("Puzzle solution recovered from tumbler's fulfill transaction");
                            NeedSave = true;
                            Status   = PaymentStateMachineStatus.PuzzleSolutionObtained;
                            var tumblingSolution = SolverClientSession.GetSolution();
                            var transaction      = PromiseClientSession.GetSignedTransaction(tumblingSolution);

                            Logs.Client.LogDebug($"TumblerCashOut 2: {transaction}");
                            Logs.Client.LogDebug($"TumblerCashOut hex 2: {transaction.ToHex()}");

                            Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerCashout, transaction.GetHash(), correlation);
                            Services.BroadcastService.BroadcastAsync(transaction).GetAwaiter().GetResult();
                        }
                    }

                    break;
                }
            }
            catch (InvalidStateException ex)
            {
                Logs.Client.LogDebug(new EventId(), ex, "Client side Invalid State, the payment is wasted");
                Status = PaymentStateMachineStatus.Wasted;
            }
            catch (Exception ex) when(ex.Message.IndexOf("invalid-state", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                Logs.Client.LogDebug(new EventId(), ex, "Tumbler side Invalid State, the payment is wasted");
                Status = PaymentStateMachineStatus.Wasted;
            }
            finally
            {
                if (previousState != Status)
                {
                    Logs.Client.LogInformation($"Status changed {previousState} => {Status}");
                }
                if (alice != null && bob != null)
                {
                    throw new InvalidOperationException("Bob and Alice have been both initialized, please report the bug to NTumbleBit developers");
                }
                if (alice != null)
                {
                    alice.Dispose();
                }
                if (bob != null)
                {
                    bob.Dispose();
                }
            }

            progressInfo.ShouldStayConnected = ShouldStayConnected();

            return(progressInfo);
        }