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