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