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 TestPuzzleSolver() { RsaKey key = TestKeys.Default; PuzzleSolution expectedSolution = null; Puzzle puzzle = key.PubKey.GeneratePuzzle(ref expectedSolution); var parameters = new SolverParameters { FakePuzzleCount = 50, RealPuzzleCount = 10, ServerKey = key.PubKey }; SolverClientSession client = new SolverClientSession(parameters); SolverServerSession server = new SolverServerSession(key, parameters); var clientEscrow = new Key(); var serverEscrow = new Key(); var escrow = CreateEscrowCoin(clientEscrow.PubKey, serverEscrow.PubKey); var redeemDestination = new Key().ScriptPubKey; client.ConfigureEscrowedCoin(escrow, clientEscrow, redeemDestination); client.AcceptPuzzle(puzzle.PuzzleValue); RoundTrip(ref client, parameters); Assert.True(client.GetInternalState().RedeemDestination == redeemDestination); PuzzleValue[] puzzles = client.GeneratePuzzles(); RoundTrip(ref client, parameters); RoundTrip(ref puzzles); server.ConfigureEscrowedCoin(escrow, serverEscrow); var commitments = server.SolvePuzzles(puzzles); RoundTrip(ref server, parameters, key); RoundTrip(ref commitments); var revelation = client.Reveal(commitments); RoundTrip(ref client, parameters); RoundTrip(ref revelation); SolutionKey[] fakePuzzleKeys = server.CheckRevelation(revelation); RoundTrip(ref server, parameters, key); RoundTrip(ref fakePuzzleKeys); BlindFactor[] blindFactors = client.GetBlindFactors(fakePuzzleKeys); RoundTrip(ref client, parameters); RoundTrip(ref blindFactors); var offerInformation = server.CheckBlindedFactors(blindFactors, FeeRate); RoundTrip(ref server, parameters, key); var clientOfferSig = client.SignOffer(offerInformation); //Verify if the scripts are correctly created var fulfill = server.FulfillOffer(clientOfferSig, new Key().ScriptPubKey, FeeRate); var offerRedeem = client.CreateOfferRedeemTransaction(FeeRate); var offerTransaction = server.GetSignedOfferTransaction(); var offerCoin = offerTransaction.Transaction.Outputs.AsCoins().First(); var resigned = offerTransaction.ReSign(client.EscrowedCoin); TransactionBuilder txBuilder = new TransactionBuilder(); txBuilder.AddCoins(client.EscrowedCoin); Assert.True(txBuilder.Verify(resigned)); resigned = fulfill.ReSign(offerCoin); txBuilder = new TransactionBuilder(); txBuilder.AddCoins(offerCoin); Assert.True(txBuilder.Verify(resigned)); var offerRedeemTx = offerRedeem.ReSign(offerCoin); txBuilder = new TransactionBuilder(); txBuilder.AddCoins(offerCoin); Assert.True(txBuilder.Verify(offerRedeemTx)); client.CheckSolutions(fulfill.Transaction); RoundTrip(ref client, parameters); var clientEscapeSignature = client.SignEscape(); var escapeTransaction = server.GetSignedEscapeTransaction(clientEscapeSignature, FeeRate, new Key().ScriptPubKey); txBuilder = new TransactionBuilder(); txBuilder.AddCoins(client.EscrowedCoin); Assert.True(txBuilder.Verify(escapeTransaction)); var solution = client.GetSolution(); RoundTrip(ref client, parameters); Assert.True(solution == expectedSolution); }
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 TestPuzzleSolver() { RsaKey key = TestKeys.Default; PuzzleSolution expectedSolution = null; Puzzle puzzle = key.PubKey.GeneratePuzzle(ref expectedSolution); var parameters = new SolverParameters { FakePuzzleCount = 50, RealPuzzleCount = 10, ServerKey = key.PubKey }; SolverClientSession client = new SolverClientSession(parameters); SolverServerSession server = new SolverServerSession(key, parameters); var clientEscrow = new Key(); var serverEscrow = new Key(); var clientRedeem = new Key(); var escrow = CreateEscrowCoin(clientEscrow.PubKey, serverEscrow.PubKey, clientRedeem.PubKey); client.ConfigureEscrowedCoin(escrow, clientEscrow, clientRedeem); client.AcceptPuzzle(puzzle.PuzzleValue); RoundTrip(ref client, parameters); PuzzleValue[] puzzles = client.GeneratePuzzles(); RoundTrip(ref client, parameters); RoundTrip(ref puzzles); server.ConfigureEscrowedCoin(escrow, serverEscrow); var commitments = server.SolvePuzzles(puzzles); RoundTrip(ref server, parameters, key); RoundTrip(ref commitments); var revelation = client.Reveal(commitments); RoundTrip(ref client, parameters); RoundTrip(ref revelation); SolutionKey[] fakePuzzleKeys = server.CheckRevelation(revelation); RoundTrip(ref server, parameters, key); RoundTrip(ref fakePuzzleKeys); BlindFactor[] blindFactors = client.GetBlindFactors(fakePuzzleKeys); RoundTrip(ref client, parameters); RoundTrip(ref blindFactors); var offerInformation = server.CheckBlindedFactors(blindFactors, FeeRate); RoundTrip(ref server, parameters, key); var clientOfferSig = client.SignOffer(offerInformation); //Verify if the scripts are correctly created var fulfill = server.FulfillOffer(clientOfferSig, new Key().ScriptPubKey, FeeRate); var offerTransaction = server.GetSignedOfferTransaction(); TransactionBuilder txBuilder = new TransactionBuilder(); txBuilder.AddCoins(client.EscrowedCoin); Assert.True(txBuilder.Verify(offerTransaction.Transaction)); txBuilder = new TransactionBuilder(); txBuilder.AddCoins(offerTransaction.Transaction.Outputs.AsCoins().ToArray()); Assert.True(txBuilder.Verify(fulfill.Transaction)); //Check if can resign fulfill in case offer get malleated offerTransaction.Transaction.LockTime = new LockTime(1); fulfill.Transaction.Inputs[0].PrevOut = offerTransaction.Transaction.Outputs.AsCoins().First().Outpoint; txBuilder = new TransactionBuilder(); txBuilder.Extensions.Add(new OfferBuilderExtension()); txBuilder.AddKeys(server.GetInternalState().FulfillKey); txBuilder.AddCoins(offerTransaction.Transaction.Outputs.AsCoins().ToArray()); txBuilder.SignTransactionInPlace(fulfill.Transaction); Assert.True(txBuilder.Verify(fulfill.Transaction)); //////////////////////////////////////////////// client.CheckSolutions(fulfill.Transaction); RoundTrip(ref client, parameters); var solution = client.GetSolution(); RoundTrip(ref client, parameters); Assert.True(solution == expectedSolution); }