Пример #1
0
            public override uint256 GetHash(Network network)
            {
                var escrow = EscrowScriptPubKeyParameters.GetFromCoin(_Escrow);
                var coin   = _Escrow.Clone();

                coin.OverrideScriptCode(escrow.GetInitiatorScriptCode());
                return(GetTransaction(network).GetSignatureHash(network, coin, SigHash.All));
            }
Пример #2
0
        private ScriptCoin CreateEscrowCoin(PubKey initiator, PubKey receiver)
        {
            var redeem     = new EscrowScriptPubKeyParameters(initiator, receiver, new LockTime(10)).ToScript();
            var scriptCoin = new Coin(new OutPoint(new uint256(RandomUtils.GetBytes(32)), 0),
                                      new TxOut
            {
                Value        = Money.Coins(1.5m),
                ScriptPubKey = redeem.Hash.ScriptPubKey
            }).ToScriptCoin(redeem);

            return(scriptCoin);
        }
Пример #3
0
        private OfferScriptPubKeyParameters CreateOfferScriptParameters()
        {
            var escrow = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin);

            return(new OfferScriptPubKeyParameters
            {
                Hashes = InternalState.SolvedPuzzles.Select(p => p.SolutionKey.GetHash()).ToArray(),
                FulfillKey = InternalState.FulfillKey.PubKey,
                Expiration = escrow.LockTime,
                RedeemKey = escrow.Initiator
            });
        }
Пример #4
0
        public TransactionSignature SignEscape()
        {
            AssertState(SolverClientStates.Completed);
            var dummy = new Transaction();

            dummy.Inputs.Add(new TxIn(InternalState.EscrowedCoin.Outpoint));
            dummy.Outputs.Add(new TxOut());

            var escrow = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin);
            var coin   = InternalState.EscrowedCoin.Clone();

            coin.OverrideScriptCode(escrow.GetInitiatorScriptCode());
            return(dummy.SignInput(InternalState.EscrowKey, coin, SigHash.None | SigHash.AnyoneCanPay));
        }
Пример #5
0
        private PubKey AssertValidSignature(TransactionSignature clientSignature, Transaction offer)
        {
            var escrow = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin);
            var coin   = InternalState.EscrowedCoin.Clone();

            coin.OverrideScriptCode(escrow.GetInitiatorScriptCode());
            var signedHash = offer.Inputs.AsIndexedInputs().First().GetSignatureHash(coin, clientSignature.SigHash);
            var clientKey  = InternalState.GetClientEscrowPubKey();

            if (!clientKey.Verify(signedHash, clientSignature.Signature))
            {
                throw new PuzzleException("invalid-client-signature");
            }
            return(clientKey);
        }
Пример #6
0
        public uint256 CreateRealHash(Transaction tx, ScriptCoin _Escrow, Money feeVariation)
        {
            /*
             * Not sure if this is best way to do this, but had to add this for step 7
             * when verifying valid Hashes, the server will have to make real hashes, but
             * it doesn't have access to RealHash class. So I created this function that
             * takes care of that
             */
            var escrow = EscrowScriptPubKeyParameters.GetFromCoin(_Escrow);
            var coin   = _Escrow.Clone();

            coin.OverrideScriptCode(escrow.GetInitiatorScriptCode());
            var Transaction = tx.Clone();

            Transaction.Outputs[0].Value -= feeVariation;
            return(Transaction.GetSignatureHash(coin, SigHash.All));
        }
Пример #7
0
        private bool IsValidSignature(PuzzleSolution solution, HashBase hash, out ECDSASignature signature)
        {
            signature = null;
            var escrow = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin);

            try
            {
                var key = new XORKey(solution);
                signature = new ECDSASignature(key.XOR(hash.Commitment.Promise));
                var ok = escrow.Initiator.Verify(hash.GetHash(), signature);
                if (!ok)
                {
                    signature = null;
                }
                return(ok);
            }
            catch
            {
            }
            return(false);
        }
        public PromiseClientSession ReceiveTumblerEscrowedCoin(ScriptCoin escrowedCoin)
        {
            AssertState(TumblerClientSessionStates.WaitingTumblerEscrow);
            var escrow = EscrowScriptPubKeyParameters.GetFromCoin(escrowedCoin);

            if (escrow == null)
            {
                throw new PuzzleException("invalid-escrow");
            }
            if (!escrowedCoin.IsP2SH || escrowedCoin.RedeemType != RedeemType.WitnessV0)
            {
                throw new PuzzleException("invalid-escrow");
            }
            var expectedEscrow = GetTumblerEscrowParameters(escrow.Initiator);

            if (escrow != expectedEscrow)
            {
                throw new PuzzleException("invalid-escrow");
            }
            if (escrowedCoin.Amount != Parameters.Denomination)
            {
                throw new PuzzleException("invalid-amount");
            }


            InternalState.Status = TumblerClientSessionStates.PromisePhase;
            var session = new PromiseClientSession(Parameters.CreatePromiseParamaters());

            session.SetChannelId(InternalState.ChannelId);

            Logs.Tumbler.LogDebug(
                $"ClientChannelNegotiation.ReceiveTumblerEscrowedCoin() - escrowedCoin.Outpoint.Hash : {escrowedCoin.Outpoint.Hash}, escrowedCoin.Outpoint.N : {escrowedCoin.Outpoint.N}, escrowKey.GetWif() : {InternalState.TumblerEscrowKey.GetWif(this.Parameters.Network)}");
            session.ConfigureEscrowedCoin(escrowedCoin, InternalState.TumblerEscrowKey);
            InternalState.TumblerEscrowKey = null;

            LogInternalState(session);

            return(session);
        }
Пример #9
0
        public TrustedBroadcastRequest CreateOfferRedeemTransaction(FeeRate feeRate)
        {
            Transaction tx = new Transaction();

            tx.LockTime = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin).LockTime;
            tx.Inputs.Add(new TxIn());
            tx.Inputs[0].Sequence = 0;
            tx.Outputs.Add(new TxOut(InternalState.OfferCoin.Amount, InternalState.RedeemDestination));
            tx.Inputs[0].ScriptSig = new Script(
                Op.GetPushOp(TrustedBroadcastRequest.PlaceholderSignature),
                Op.GetPushOp(InternalState.OfferCoin.Redeem.ToBytes()));
            tx.Outputs[0].Value -= feeRate.GetFee(tx.GetVirtualSize());

            var redeemTransaction = new TrustedBroadcastRequest
            {
                Key = InternalState.EscrowKey,
                PreviousScriptPubKey = InternalState.OfferCoin.ScriptPubKey,
                Transaction          = tx
            };

            return(redeemTransaction);
        }
Пример #10
0
        public TransactionSignature SignOffer(OfferInformation offerInformation)
        {
            if (offerInformation == null)
            {
                throw new ArgumentNullException(nameof(offerInformation));
            }
            AssertState(SolverClientStates.WaitingOffer);

            var offerScript = new OfferScriptPubKeyParameters
            {
                Hashes     = _PuzzleElements.OfType <RealPuzzle>().Select(p => p.Commitment.KeyHash).ToArray(),
                FulfillKey = offerInformation.FulfillKey,
                Expiration = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin).LockTime,
                RedeemKey  = InternalState.EscrowKey.PubKey
            }.ToScript();

            var escrowCoin = InternalState.EscrowedCoin;
            var txOut      = new TxOut(escrowCoin.Amount - offerInformation.Fee, offerScript.WitHash.ScriptPubKey.Hash);
            var offerCoin  = new Coin(escrowCoin.Outpoint, txOut).ToScriptCoin(offerScript);


            Transaction tx = new Transaction();

            tx.Inputs.Add(new TxIn(escrowCoin.Outpoint));
            tx.Outputs.Add(offerCoin.TxOut);

            var escrow = EscrowScriptPubKeyParameters.GetFromCoin(escrowCoin);

            escrowCoin = escrowCoin.Clone();
            escrowCoin.OverrideScriptCode(escrow.GetInitiatorScriptCode());
            var signature = tx.Inputs.AsIndexedInputs().First().Sign(InternalState.EscrowKey, escrowCoin, SigHash.All);

            InternalState.OfferCoin = offerCoin;
            InternalState.Status    = SolverClientStates.WaitingPuzzleSolutions;
            return(signature);
        }
Пример #11
0
        public async Task <uint160.MutableUint160> BeginOpenChannel(
            [ModelBinder(BinderType = typeof(TumblerParametersModelBinder))]
            ClassicTumblerParameters tumblerId,
            [FromBody] OpenChannelRequest request)
        {
            if (tumblerId == null)
            {
                throw new ArgumentNullException("tumblerId");
            }
            var height = Services.BlockExplorerService.GetCurrentHeight();

            if (Repository.IsUsed(request.CycleStart, request.Nonce))
            {
                throw new ActionResultException(BadRequest("duplicate-query"));
            }
            var cycle = GetCycle(request.CycleStart);

            if (!cycle.IsInPhase(CyclePhase.TumblerChannelEstablishment, height))
            {
                throw new ActionResultException(BadRequest("invalid-phase"));
            }
            var fee = await Services.FeeService.GetFeeRateAsync();

            try
            {
                if (!Parameters.VoucherKey.PublicKey.Verify(request.Signature, NBitcoin.Utils.ToBytes((uint)request.CycleStart, true), request.Nonce))
                {
                    throw new ActionResultException(BadRequest("incorrect-voucher"));
                }
                if (!Repository.MarkUsedNonce(request.CycleStart, request.Nonce))
                {
                    throw new ActionResultException(BadRequest("duplicate-query"));
                }

                var escrowKey = new Key();

                var escrow = new EscrowScriptPubKeyParameters();
                escrow.LockTime  = cycle.GetTumblerLockTime();
                escrow.Receiver  = request.EscrowKey;
                escrow.Initiator = escrowKey.PubKey;
                var channelId = new uint160(RandomUtils.GetBytes(20));
                Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Asked to open channel");
                var txOut = new TxOut(Parameters.Denomination, escrow.ToScript().WitHash.ScriptPubKey.Hash);

                var unused = Services.WalletService.FundTransactionAsync(txOut, fee)
                             .ContinueWith(async(Task <Transaction> task) =>
                {
                    try
                    {
                        var tx          = await task.ConfigureAwait(false);
                        var correlation = new CorrelationId(channelId);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, tx.GetHash(), correlation);

                        //Logging/Broadcast per funding TX one time
                        if (Repository.MarkUsedNonce(cycle.Start, new uint160(tx.GetHash().ToBytes().Take(20).ToArray())))
                        {
                            var bobCount = Parameters.CountEscrows(tx, Client.Identity.Bob);
                            Logs.Tumbler.LogInformation($"Cycle {cycle.Start} channel created {tx.GetHash()} with {bobCount} users");
                            await Services.BroadcastService.BroadcastAsync(tx).ConfigureAwait(false);
                        }

                        await Services.BlockExplorerService.TrackAsync(txOut.ScriptPubKey).ConfigureAwait(false);
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, txOut.ScriptPubKey, correlation);
                        var coin    = tx.Outputs.AsCoins().First(o => o.ScriptPubKey == txOut.ScriptPubKey && o.TxOut.Value == txOut.Value);
                        var session = new PromiseServerSession(Parameters.CreatePromiseParamaters());
                        var redeem  = await Services.WalletService.GenerateAddressAsync().ConfigureAwait(false);
                        session.ConfigureEscrowedCoin(channelId, coin.ToScriptCoin(escrow.ToScript()), escrowKey, redeem.ScriptPubKey);
                        var redeemTx = session.CreateRedeemTransaction(fee);
                        Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.TumblerRedeem, correlation, redeemTx);
                        Repository.Save(cycle.Start, session);
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerRedeem, redeem.ScriptPubKey, correlation);
                    }
                    catch (Exception ex)
                    {
                        Logs.Tumbler.LogCritical(new EventId(), ex, "Error during escrow transaction callback");
                    }
                });
                return(channelId.AsBitcoinSerializable());
            }
            catch (NotEnoughFundsException ex)
            {
                Logs.Tumbler.LogInformation(ex.Message);
                throw new ActionResultException(BadRequest("tumbler-insufficient-funds"));
            }
        }
Пример #12
0
        public async Task <PuzzleSolution> SignVoucher(
            [ModelBinder(BinderType = typeof(TumblerParametersModelBinder))]
            ClassicTumblerParameters tumblerId,
            [FromBody] SignVoucherRequest request)
        {
            if (tumblerId == null)
            {
                throw new ArgumentNullException("tumblerId");
            }
            if (request.UnsignedVoucher == null)
            {
                throw new ActionResultException(BadRequest("Missing UnsignedVoucher"));
            }
            if (request.MerkleProof == null)
            {
                throw new ActionResultException(BadRequest("Missing MerkleProof"));
            }
            if (request.Transaction == null)
            {
                throw new ActionResultException(BadRequest("Missing Transaction"));
            }
            if (request.ClientEscrowKey == null)
            {
                throw new ActionResultException(BadRequest("Missing ClientEscrowKey"));
            }
            if (request.ChannelId == null)
            {
                throw new ActionResultException(BadRequest("Missing ChannelId"));
            }

            var cycle  = GetCycle(request.Cycle);
            var height = Services.BlockExplorerService.GetCurrentHeight();

            if (!cycle.IsInPhase(CyclePhase.ClientChannelEstablishment, height))
            {
                throw new ActionResultException(BadRequest("invalid-phase"));
            }

            if (request.MerkleProof.PartialMerkleTree
                .GetMatchedTransactions()
                .FirstOrDefault() != request.Transaction.GetHash() || !request.MerkleProof.Header.CheckProofOfWork())
            {
                Logs.Tumbler.LogDebug("Invalid transaction merkle proof");
                throw new ActionResultException(BadRequest("invalid-merkleproof"));
            }

            var confirmations = Services.BlockExplorerService.GetBlockConfirmations(request.MerkleProof.Header.GetHash());

            if ((confirmations < Parameters.CycleGenerator.FirstCycle.SafetyPeriodDuration))
            {
                Logs.Tumbler.LogDebug("Not enough confirmations");
                throw new ActionResultException(BadRequest("not-enough-confirmation"));
            }

            var transaction = request.Transaction;

            if (transaction.Outputs.Count > 2)
            {
                Logs.Tumbler.LogDebug("Incorrect number of outputs");
                throw new ActionResultException(BadRequest("invalid-transaction"));
            }

            var key = Repository.GetKey(cycle.Start, request.KeyReference);

            if (Repository.IsUsed(cycle.Start, request.ChannelId))
            {
                throw new ActionResultException(BadRequest("duplicate-query"));
            }

            var expectedEscrow = new EscrowScriptPubKeyParameters(request.ClientEscrowKey, key.PubKey, cycle.GetClientLockTime());

            var expectedTxOut = new TxOut(Parameters.Denomination + Parameters.Fee, expectedEscrow.ToScript().WitHash.ScriptPubKey.Hash);
            var escrowedCoin  =
                transaction
                .Outputs
                .AsCoins()
                .Where(c => c.TxOut.Value == expectedTxOut.Value &&
                       c.TxOut.ScriptPubKey == expectedTxOut.ScriptPubKey)
                .Select(c => c.ToScriptCoin(expectedEscrow.ToScript()))
                .FirstOrDefault();

            if (escrowedCoin == null)
            {
                Logs.Tumbler.LogDebug("Could not find escrowed coin");
                throw new ActionResultException(BadRequest("invalid-transaction"));
            }

            var solverServerSession = new SolverServerSession(Runtime.TumblerKey, Parameters.CreateSolverParamaters());

            solverServerSession.SetChannelId(request.ChannelId);
            solverServerSession.ConfigureEscrowedCoin(escrowedCoin, key);
            await Services.BlockExplorerService.TrackAsync(escrowedCoin.ScriptPubKey);

            if (!await Services.BlockExplorerService.TrackPrunedTransactionAsync(request.Transaction, request.MerkleProof))
            {
                throw new ActionResultException(BadRequest("invalid-merkleproof"));
            }

            //Without this one, someone could spam the nonce db by replaying this request with different channelId
            if (!Repository.MarkUsedNonce(cycle.Start, Hashes.Hash160(escrowedCoin.Outpoint.ToBytes())))
            {
                throw new ActionResultException(BadRequest("duplicate-query"));
            }

            AssertNotDuplicateQuery(cycle.Start, request.ChannelId);

            Repository.Save(cycle.Start, solverServerSession);
            Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Proof of Escrow signed for " + transaction.GetHash());

            var correlation = GetCorrelation(solverServerSession);

            Tracker.AddressCreated(cycle.Start, TransactionType.ClientEscrow, escrowedCoin.ScriptPubKey, correlation);
            Tracker.TransactionCreated(cycle.Start, TransactionType.ClientEscrow, request.Transaction.GetHash(), correlation);
            var solution = request.UnsignedVoucher.WithRsaKey(Runtime.VoucherKey.PubKey).Solve(Runtime.VoucherKey);

            return(solution);
        }
Пример #13
0
 public PubKey GetClientEscrowPubKey()
 {
     return(EscrowScriptPubKeyParameters.GetFromCoin(EscrowedCoin).Initiator);
 }
Пример #14
0
        public OfferInformation CheckBlindedFactors(BlindFactor[] blindFactors, FeeRate feeRate)
        {
            if (blindFactors == null)
            {
                throw new ArgumentNullException(nameof(blindFactors));
            }
            if (blindFactors.Length != Parameters.RealPuzzleCount)
            {
                throw new ArgumentException("Expecting " + Parameters.RealPuzzleCount + " blind factors");
            }
            AssertState(SolverServerStates.WaitingBlindFactor);
            Puzzle unblindedPuzzle = null;
            int    y = 0;

            for (int i = 0; i < Parameters.RealPuzzleCount; i++)
            {
                var solvedPuzzle = InternalState.SolvedPuzzles[i];
                var unblinded    = new Puzzle(Parameters.ServerKey, solvedPuzzle.Puzzle).Unblind(blindFactors[i]);
                if (unblindedPuzzle == null)
                {
                    unblindedPuzzle = unblinded;
                }
                else if (unblinded != unblindedPuzzle)
                {
                    throw new PuzzleException("Invalid blind factor");
                }
                y++;
            }

            InternalState.FulfillKey = new Key();

            Transaction dummy = new Transaction();

            dummy.AddInput(new TxIn(InternalState.EscrowedCoin.Outpoint));
            dummy.Inputs[0].ScriptSig = new Script(
                Op.GetPushOp(TrustedBroadcastRequest.PlaceholderSignature),
                Op.GetPushOp(TrustedBroadcastRequest.PlaceholderSignature),
                Op.GetPushOp(InternalState.EscrowedCoin.Redeem.ToBytes())
                );
            dummy.Inputs[0].Witnessify();
            dummy.AddOutput(new TxOut(InternalState.EscrowedCoin.Amount, new Key().ScriptPubKey.Hash));

            var offerTransactionFee = feeRate.GetFee(dummy.GetVirtualSize());


            var escrow            = InternalState.EscrowedCoin;
            var escrowInformation = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin);
            var redeem            = new OfferScriptPubKeyParameters
            {
                Hashes     = InternalState.SolvedPuzzles.Select(p => p.SolutionKey.GetHash()).ToArray(),
                FulfillKey = InternalState.FulfillKey.PubKey,
                Expiration = escrowInformation.LockTime,
                RedeemKey  = escrowInformation.Initiator
            }.ToScript();
            var txOut = new TxOut(escrow.Amount - offerTransactionFee, redeem.WitHash.ScriptPubKey.Hash);

            InternalState.OfferCoin = new Coin(escrow.Outpoint, txOut).ToScriptCoin(redeem);
            InternalState.Status    = SolverServerStates.WaitingFulfillment;
            return(new OfferInformation
            {
                FulfillKey = InternalState.FulfillKey.PubKey,
                Fee = offerTransactionFee
            });
        }
Пример #15
0
 private static CorrelationId GetCorrelation(SolverServerSession session)
 {
     return(EscrowScriptPubKeyParameters.GetFromCoin(session.EscrowedCoin).GetCorrelation());
 }
Пример #16
0
        public ScriptCoinModel OpenChannel(
            [ModelBinder(BinderType = typeof(TumblerParametersModelBinder))]
            ClassicTumblerParameters tumblerId,
            [FromBody] OpenChannelRequest request)
        {
            if (tumblerId == null)
            {
                throw new ArgumentNullException("tumblerId");
            }
            var height = Services.BlockExplorerService.GetCurrentHeight();
            var cycle  = GetCycle(request.CycleStart);

            if (!cycle.IsInPhase(CyclePhase.TumblerChannelEstablishment, height))
            {
                throw new ActionResultException(BadRequest("invalid-phase"));
            }
            var fee = Services.FeeService.GetFeeRate();

            try
            {
                if (!Parameters.VoucherKey.Verify(request.Signature, NBitcoin.Utils.ToBytes((uint)request.CycleStart, true), request.Nonce))
                {
                    throw new ActionResultException(BadRequest("incorrect-voucher"));
                }
                if (!Repository.MarkUsedNonce(request.CycleStart, request.Nonce))
                {
                    throw new ActionResultException(BadRequest("nonce-already-used"));
                }

                var escrowKey = new Key();

                var escrow = new EscrowScriptPubKeyParameters();
                escrow.LockTime  = cycle.GetTumblerLockTime();
                escrow.Receiver  = request.EscrowKey;
                escrow.Initiator = escrowKey.PubKey;

                Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Asked to open channel");
                var txOut              = new TxOut(Parameters.Denomination, escrow.ToScript().Hash);
                var tx                 = Services.WalletService.FundTransaction(txOut, fee);
                var correlation        = escrow.GetCorrelation();
                var escrowTumblerLabel = $"Cycle {cycle.Start} Tumbler Escrow";
                Services.BlockExplorerService.Track(txOut.ScriptPubKey);

                Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, txOut.ScriptPubKey, correlation);
                Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, tx.GetHash(), correlation);
                Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Channel created " + tx.GetHash());

                var coin = tx.Outputs.AsCoins().First(o => o.ScriptPubKey == txOut.ScriptPubKey && o.TxOut.Value == txOut.Value);

                var session = new PromiseServerSession(Parameters.CreatePromiseParamaters());
                var redeem  = Services.WalletService.GenerateAddress();
                session.ConfigureEscrowedCoin(coin.ToScriptCoin(escrow.ToScript()), escrowKey, redeem.ScriptPubKey);
                Repository.Save(cycle.Start, session);

                Services.BroadcastService.Broadcast(tx);

                var redeemTx = session.CreateRedeemTransaction(fee);
                Tracker.AddressCreated(cycle.Start, TransactionType.TumblerRedeem, redeem.ScriptPubKey, correlation);
                Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.TumblerRedeem, correlation, redeemTx);
                return(new ScriptCoinModel(session.EscrowedCoin));
            }
            catch (PuzzleException)
            {
                throw new ActionResultException(BadRequest("incorrect-voucher"));
            }
            catch (NotEnoughFundsException ex)
            {
                Logs.Tumbler.LogInformation(ex.Message);
                throw new ActionResultException(BadRequest("tumbler-insufficient-funds"));
            }
        }
Пример #17
0
        public PuzzleSolution SignVoucher(
            [ModelBinder(BinderType = typeof(TumblerParametersModelBinder))]
            ClassicTumblerParameters tumblerId,
            [FromBody] SignVoucherRequest request)
        {
            if (tumblerId == null)
            {
                throw new ArgumentNullException("tumblerId");
            }
            if (request.UnsignedVoucher == null)
            {
                throw new ActionResultException(BadRequest("Missing UnsignedVoucher"));
            }
            if (request.MerkleProof == null)
            {
                throw new ActionResultException(BadRequest("Missing MerkleProof"));
            }
            if (request.Transaction == null)
            {
                throw new ActionResultException(BadRequest("Missing Transaction"));
            }
            if (request.ClientEscrowKey == null)
            {
                throw new ActionResultException(BadRequest("Missing ClientEscrowKey"));
            }

            if (request.MerkleProof.PartialMerkleTree
                .GetMatchedTransactions()
                .FirstOrDefault() != request.Transaction.GetHash() || !request.MerkleProof.Header.CheckProofOfWork())
            {
                throw new ActionResultException(BadRequest("invalid-merkleproof"));
            }

            var confirmations = Services.BlockExplorerService.GetBlockConfirmations(request.MerkleProof.Header.GetHash());

            if ((confirmations < Parameters.CycleGenerator.FirstCycle.SafetyPeriodDuration))
            {
                throw new ActionResultException(BadRequest("not-enough-confirmation"));
            }

            var transaction = request.Transaction;

            if (transaction.Outputs.Count > 2)
            {
                throw new ActionResultException(BadRequest("invalid-transaction"));
            }

            var cycle  = GetCycle(request.Cycle);
            var height = Services.BlockExplorerService.GetCurrentHeight();

            if (!cycle.IsInPhase(CyclePhase.ClientChannelEstablishment, height))
            {
                throw new ActionResultException(BadRequest("invalid-phase"));
            }


            var key = Repository.GetKey(cycle.Start, request.KeyReference);

            var expectedEscrow = new EscrowScriptPubKeyParameters(request.ClientEscrowKey, key.PubKey, cycle.GetClientLockTime());

            var expectedTxOut = new TxOut(Parameters.Denomination + Parameters.Fee, expectedEscrow.ToScript().Hash);
            var escrowedCoin  =
                transaction
                .Outputs
                .AsCoins()
                .Where(c => c.TxOut.Value == expectedTxOut.Value &&
                       c.TxOut.ScriptPubKey == expectedTxOut.ScriptPubKey)
                .Select(c => c.ToScriptCoin(expectedEscrow.ToScript()))
                .FirstOrDefault();

            if (escrowedCoin == null)
            {
                throw new ActionResultException(BadRequest("invalid-transaction"));
            }

            try
            {
                var solverServerSession = new SolverServerSession(Runtime.TumblerKey, Parameters.CreateSolverParamaters());
                solverServerSession.ConfigureEscrowedCoin(escrowedCoin, key);

                Services.BlockExplorerService.Track(escrowedCoin.ScriptPubKey);
                if (!Services.BlockExplorerService.TrackPrunedTransaction(request.Transaction, request.MerkleProof))
                {
                    throw new ActionResultException(BadRequest("invalid-merkleproof"));
                }

                if (!Repository.MarkUsedNonce(cycle.Start, new uint160(key.PubKey.Hash.ToBytes())))
                {
                    throw new ActionResultException(BadRequest("invalid-transaction"));
                }
                Repository.Save(cycle.Start, solverServerSession);
                Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Proof of Escrow signed for " + transaction.GetHash());

                var correlation = GetCorrelation(solverServerSession);
                Tracker.AddressCreated(cycle.Start, TransactionType.ClientEscrow, escrowedCoin.ScriptPubKey, correlation);
                Tracker.TransactionCreated(cycle.Start, TransactionType.ClientEscrow, request.Transaction.GetHash(), correlation);
                var solution = request.UnsignedVoucher.WithRsaKey(Runtime.VoucherKey.PubKey).Solve(Runtime.VoucherKey);
                return(solution);
            }
            catch (PuzzleException)
            {
                throw new ActionResultException(BadRequest("invalid-transaction"));
            }
        }