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(); } } }
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); }
public void TestPuzzlePromise() { RsaKey key = TestKeys.Default; Key serverEscrow = new Key(); Key clientEscrow = new Key(); var parameters = new PromiseParameters(key.PubKey) { FakeTransactionCount = 5, RealTransactionCount = 5 }; var client = new PromiseClientSession(parameters); var server = new PromiseServerSession(parameters); var coin = CreateEscrowCoin(serverEscrow.PubKey, clientEscrow.PubKey); client.ConfigureEscrowedCoin(coin, clientEscrow); SignaturesRequest request = client.CreateSignatureRequest(clientEscrow.PubKey.Hash, FeeRate); RoundTrip(ref client, parameters); RoundTrip(ref request); server.ConfigureEscrowedCoin(coin, serverEscrow, new Key().ScriptPubKey); PuzzlePromise.ServerCommitment[] commitments = server.SignHashes(request); RoundTrip(ref server, parameters); RoundTrip(ref commitments); PuzzlePromise.ClientRevelation revelation = client.Reveal(commitments); RoundTrip(ref client, parameters); RoundTrip(ref revelation); ServerCommitmentsProof proof = server.CheckRevelation(revelation); RoundTrip(ref server, parameters); RoundTrip(ref proof); var puzzleToSolve = client.CheckCommitmentProof(proof); RoundTrip(ref client, parameters); Assert.NotNull(puzzleToSolve); var solution = key.SolvePuzzle(puzzleToSolve); var transactions = client.GetSignedTransactions(solution).ToArray(); RoundTrip(ref client, parameters); Assert.True(transactions.Length == parameters.RealTransactionCount); var escrow = server.GetInternalState().EscrowedCoin; // In case things do not go well and timeout is hit... var redeemTransaction = server.CreateRedeemTransaction(FeeRate); var resigned = redeemTransaction.ReSign(escrow); TransactionBuilder bb = new TransactionBuilder(); bb.AddCoins(server.GetInternalState().EscrowedCoin); Assert.True(bb.Verify(resigned)); //Check can ve reclaimed if malleated bb = new TransactionBuilder(); escrow.Outpoint = new OutPoint(escrow.Outpoint.Hash, 10); bb.AddCoins(escrow); resigned = redeemTransaction.ReSign(escrow); Assert.False(bb.Verify(redeemTransaction.Transaction)); Assert.True(bb.Verify(resigned)); }
public void Update(ILogger logger) { logger = logger ?? new NullLogger(); 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)); } logger.LogInformation("Cycle " + cycle.Start + " in phase " + Enum.GetName(typeof(CyclePhase), phase) + ", ending in " + (cycle.GetPeriods().GetPeriod(phase).End - height) + " blocks"); FeeRate feeRate = null; switch (phase) { case CyclePhase.Registration: if (ClientChannelNegotiation == null) { //Client asks for voucher var voucherResponse = BobClient.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); logger.LogInformation("Registration Complete"); } break; case CyclePhase.ClientChannelEstablishment: if (ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingTumblerClientTransactionKey) { var key = AliceClient.RequestTumblerEscrowKey(cycle.Start); ClientChannelNegotiation.ReceiveTumblerEscrowKey(key.PubKey, key.KeyIndex); //Client create the escrow var txout = ClientChannelNegotiation.BuildClientEscrowTxOut(); feeRate = GetFeeRate(); var clientEscrowTx = Services.WalletService.FundTransaction(txout, feeRate); if (clientEscrowTx == null) { logger.LogInformation("Not enough funds in the wallet to tumble"); break; } SolverClientSession = ClientChannelNegotiation.SetClientSignedTransaction(clientEscrowTx); var redeem = SolverClientSession.CreateRedeemTransaction(feeRate, Services.WalletService.GenerateAddress($"Cycle {cycle.Start} Client Redeem").ScriptPubKey); var escrowLabel = $"Cycle {cycle.Start} Client Escrow"; Services.BlockExplorerService.Track(escrowLabel, redeem.PreviousScriptPubKey); Services.BroadcastService.Broadcast(escrowLabel, clientEscrowTx); Services.TrustedBroadcastService.Broadcast($"Cycle {cycle.Start} Client Redeem (locked until {redeem.Transaction.LockTime})", redeem); logger.LogInformation("Client escrow broadcasted " + clientEscrowTx.GetHash()); logger.LogInformation("Client escrow redeem " + redeem.Transaction.GetHash() + " will be broadcast later if tumbler unresponsive"); } else if (ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingSolvedVoucher) { TransactionInformation clientTx = GetTransactionInformation(SolverClientSession.EscrowedCoin, true); var state = ClientChannelNegotiation.GetInternalState(); if (clientTx != null && clientTx.Confirmations >= cycle.SafetyPeriodDuration) { //Client asks the public key of the Tumbler and sends its own var aliceEscrowInformation = ClientChannelNegotiation.GenerateClientTransactionKeys(); var voucher = AliceClient.SignVoucher(new Models.SignVoucherRequest { MerkleProof = clientTx.MerkleProof, Transaction = clientTx.Transaction, KeyReference = state.TumblerEscrowKeyReference, ClientEscrowInformation = aliceEscrowInformation, TumblerEscrowPubKey = state.ClientEscrowInformation.OtherEscrowKey }); ClientChannelNegotiation.CheckVoucherSolution(voucher); logger.LogInformation("Voucher solution obtained"); } } break; case CyclePhase.TumblerChannelEstablishment: if (ClientChannelNegotiation != null && ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingGenerateTumblerTransactionKey) { //Client asks the Tumbler to make a channel var bobEscrowInformation = ClientChannelNegotiation.GetOpenChannelRequest(); var tumblerInformation = BobClient.OpenChannel(bobEscrowInformation); PromiseClientSession = ClientChannelNegotiation.ReceiveTumblerEscrowedCoin(tumblerInformation); //Tell to the block explorer we need to track that address (for checking if it is confirmed in payment phase) var escrowTumblerLabel = $"Cycle {cycle.Start} Tumbler Escrow"; Services.BlockExplorerService.Track(escrowTumblerLabel, PromiseClientSession.EscrowedCoin.ScriptPubKey); //Channel is done, now need to run the promise protocol to get valid puzzle var cashoutDestination = DestinationWallet.GetNewDestination(); feeRate = GetFeeRate(); var sigReq = PromiseClientSession.CreateSignatureRequest(cashoutDestination, feeRate); var commiments = BobClient.SignHashes(cycle.Start, PromiseClientSession.Id, sigReq); var revelation = PromiseClientSession.Reveal(commiments); var proof = BobClient.CheckRevelation(cycle.Start, PromiseClientSession.Id, revelation); var puzzle = PromiseClientSession.CheckCommitmentProof(proof); SolverClientSession.AcceptPuzzle(puzzle); logger.LogInformation("Tumbler escrow broadcasted " + PromiseClientSession.EscrowedCoin.Outpoint.Hash); } 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) { if (tumblerTx != null) { logger.LogInformation("Tumbler escrow " + tumblerTx.Transaction.GetHash() + " expecting " + cycle.SafetyPeriodDuration + " current is " + tumblerTx.Confirmations); } else { logger.LogInformation("Tumbler escrow not found"); } return; } if (SolverClientSession.Status == SolverClientStates.WaitingGeneratePuzzles) { logger.LogInformation("Tumbler escrow confirmed " + tumblerTx.Transaction.GetHash()); feeRate = GetFeeRate(); var puzzles = SolverClientSession.GeneratePuzzles(); var commmitments = AliceClient.SolvePuzzles(cycle.Start, SolverClientSession.Id, puzzles); var revelation2 = SolverClientSession.Reveal(commmitments); var solutionKeys = AliceClient.CheckRevelation(cycle.Start, SolverClientSession.Id, revelation2); var blindFactors = SolverClientSession.GetBlindFactors(solutionKeys); var offerInformation = AliceClient.CheckBlindFactors(cycle.Start, SolverClientSession.Id, blindFactors); var offerSignature = SolverClientSession.SignOffer(offerInformation); var offerRedeem = SolverClientSession.CreateOfferRedeemTransaction(feeRate, Services.WalletService.GenerateAddress($"Cycle {cycle.Start} Tumbler Redeem").ScriptPubKey); //May need to find solution in the fulfillment transaction var offerLabel = $"Cycle {cycle.Start} Client Offer Redeem (locked until {offerRedeem.Transaction.LockTime})"; Services.BlockExplorerService.Track(offerLabel, offerRedeem.PreviousScriptPubKey); Services.TrustedBroadcastService.Broadcast(offerLabel, offerRedeem); logger.LogInformation("Offer redeem " + offerRedeem.Transaction.GetHash() + " locked until " + offerRedeem.Transaction.LockTime.Height); try { solutionKeys = AliceClient.FulfillOffer(cycle.Start, SolverClientSession.Id, offerSignature); SolverClientSession.CheckSolutions(solutionKeys); logger.LogInformation("Solution recovered from cooperative tumbler"); } catch { logger.LogWarning("Uncooperative tumbler detected, keep connection open."); } logger.LogInformation("Payment completed"); } } 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.GetOfferScriptPubKey(), false); SolverClientSession.CheckSolutions(transactions.Select(t => t.Transaction).ToArray()); logger.LogInformation("Solution recovered from blockchain transaction"); } if (SolverClientSession.Status == SolverClientStates.Completed) { var tumblingSolution = SolverClientSession.GetSolution(); var transaction = PromiseClientSession.GetSignedTransaction(tumblingSolution); Services.BroadcastService.Broadcast($"Cycle {cycle.Start} Client Cashout", transaction); logger.LogInformation("Client Cashout completed " + transaction.GetHash()); } } break; } }
public void TestPuzzlePromise() { RsaKey key = TestKeys.Default; Key serverEscrow = new Key(); Key clientEscrow = new Key(); var parameters = new PromiseParameters(key.PubKey) { FakeTransactionCountPerLevel = 5, RealTransactionCountPerLevel = 5, PaymentsCount = 5 //Not sure if this is the way to go. }; var client = new PromiseClientSession(parameters); var server = new PromiseServerSession(parameters); var coin = CreateEscrowCoin(serverEscrow.PubKey, clientEscrow.PubKey); client.ConfigureEscrowedCoin(coin, clientEscrow); SignaturesRequest request = client.CreateSignatureRequest(clientEscrow.PubKey.Hash, FeeRate); RoundTrip(ref client, parameters); RoundTrip(ref request); server.ConfigureEscrowedCoin(uint160.Zero, coin, serverEscrow, new Key().ScriptPubKey); PuzzlePromise.ServerCommitment[][] commitments = server.SignHashes(request); RoundTrip(ref server, parameters); RoundTrip(ref commitments); PuzzlePromise.ClientRevelation revelation = client.Reveal(commitments); RoundTrip(ref client, parameters); RoundTrip(ref revelation); ServerCommitmentsProof proof = server.CheckRevelation(revelation, clientEscrow.PubKey.Hash, FeeRate); RoundTrip(ref server, parameters); RoundTrip(ref proof); var puzzlesToSolve = client.CheckCommitmentProof(proof); RoundTrip(ref client, parameters); foreach (var puzzle in puzzlesToSolve) { Assert.NotNull(puzzle); } for (int i = 0; i < puzzlesToSolve.Length; i++) { // Doesn't work for now! Need to figure how Bob will be spending the puzzles. var solution = key.SolvePuzzle(puzzlesToSolve[i]); // I'm not sure if GetSignedTransactions should handle all payments or only one payment at a time. var transactions = client.GetSignedTransactions(solution, i).ToArray(); RoundTrip(ref client, parameters); Assert.True(transactions.Length == parameters.RealTransactionCountPerLevel); } var escrow = server.GetInternalState().EscrowedCoin; // In case things do not go well and timeout is hit... var redeemTransaction = server.CreateRedeemTransaction(FeeRate); var resigned = redeemTransaction.ReSign(escrow); TransactionBuilder bb = new TransactionBuilder(); bb.AddCoins(server.GetInternalState().EscrowedCoin); Assert.True(bb.Verify(resigned)); //Check can ve reclaimed if malleated bb = new TransactionBuilder(); escrow.Outpoint = new OutPoint(escrow.Outpoint.Hash, 10); bb.AddCoins(escrow); resigned = redeemTransaction.ReSign(escrow); Assert.False(bb.Verify(redeemTransaction.Transaction)); Assert.True(bb.Verify(resigned)); }