Example #1
0
        public CcjRound(RPCClient rpc, UtxoReferee utxoReferee, CcjRoundConfig config)
        {
            try
            {
                Interlocked.Increment(ref RoundCount);
                RoundId = Interlocked.Read(ref RoundCount);

                RpcClient   = Guard.NotNull(nameof(rpc), rpc);
                UtxoReferee = Guard.NotNull(nameof(utxoReferee), utxoReferee);
                Guard.NotNull(nameof(config), config);

                ConfirmationTarget            = (int)config.ConfirmationTarget;
                CoordinatorFeePercent         = (decimal)config.CoordinatorFeePercent;
                AnonymitySet                  = (int)config.AnonymitySet;
                InputRegistrationTimeout      = TimeSpan.FromSeconds((long)config.InputRegistrationTimeout);
                ConnectionConfirmationTimeout = TimeSpan.FromSeconds((long)config.ConnectionConfirmationTimeout);
                OutputRegistrationTimeout     = TimeSpan.FromSeconds((long)config.OutputRegistrationTimeout);
                SigningTimeout                = TimeSpan.FromSeconds((long)config.SigningTimeout);

                PhaseLock  = new object();
                Phase      = CcjRoundPhase.InputRegistration;
                StatusLock = new object();
                Status     = CcjRoundStatus.NotStarted;

                RegisteredUnblindedSignatures     = new List <UnblindedSignature>();
                RegisteredUnblindedSignaturesLock = new object();

                MixingLevels = new MixingLevels(config.Denomination, new Signer(new Key()));
                for (int i = 0; i < config.MaximumMixingLevelCount - 1; i++)
                {
                    MixingLevels.AddNewLevel();
                }

                _unsignedCoinJoinHex = null;

                UnsignedCoinJoin = null;
                SignedCoinJoin   = null;

                Alices = new List <Alice>();
                Bobs   = new List <Bob>();

                Logger.LogInfo <CcjRound>($"New round ({RoundId}) is created.\n\t" +
                                          $"BaseDenomination: {MixingLevels.GetBaseDenomination().ToString(false, true)} BTC.\n\t" +
                                          $"{nameof(ConfirmationTarget)}: {ConfirmationTarget}.\n\t" +
                                          $"{nameof(CoordinatorFeePercent)}: {CoordinatorFeePercent}%.\n\t" +
                                          $"{nameof(AnonymitySet)}: {AnonymitySet}.");
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjRound>($"Round ({RoundId}): Could not create.");
                Logger.LogError <CcjRound>(ex);
                throw;
            }
        }
Example #2
0
        public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase, Money feePerInputs = null, Money feePerOutputs = null)
        {
            using (await RoundSynchronizerLock.LockAsync())
            {
                try
                {
                    Logger.LogInfo <CcjRound>($"Round ({RoundId}): Phase change requested: {expectedPhase.ToString()}.");

                    if (Status == CcjRoundStatus.NotStarted)                     // So start the input registration phase
                    {
                        if (expectedPhase != CcjRoundPhase.InputRegistration)
                        {
                            return;
                        }

                        // Calculate fees.
                        if (feePerInputs is null || feePerOutputs is null)
                        {
                            (Money feePerInputs, Money feePerOutputs)fees = await CalculateFeesAsync(RpcClient, ConfirmationTarget);

                            FeePerInputs  = feePerInputs ?? fees.feePerInputs;
                            FeePerOutputs = feePerOutputs ?? fees.feePerOutputs;
                        }
                        else
                        {
                            FeePerInputs  = feePerInputs;
                            FeePerOutputs = feePerOutputs;
                        }

                        Status = CcjRoundStatus.Running;
                    }
                    else if (Status != CcjRoundStatus.Running)                     // Aborted or succeeded, swallow
                    {
                        return;
                    }
                    else if (Phase == CcjRoundPhase.InputRegistration)
                    {
                        if (expectedPhase != CcjRoundPhase.ConnectionConfirmation)
                        {
                            return;
                        }

                        Phase = CcjRoundPhase.ConnectionConfirmation;
                    }
                    else if (Phase == CcjRoundPhase.ConnectionConfirmation)
                    {
                        if (expectedPhase != CcjRoundPhase.OutputRegistration)
                        {
                            return;
                        }

                        Phase = CcjRoundPhase.OutputRegistration;
                    }
                    else if (Phase == CcjRoundPhase.OutputRegistration)
                    {
                        if (expectedPhase != CcjRoundPhase.Signing)
                        {
                            return;
                        }

                        // Build CoinJoin:

                        Money newDenomination = CalculateNewDenomination();
                        var   transaction     = Network.Consensus.ConsensusFactory.CreateTransaction();

                        // 2. Add Bob outputs.
                        foreach (Bob bob in Bobs.Where(x => x.Level == MixingLevels.GetBaseLevel()))
                        {
                            transaction.Outputs.AddWithOptimize(newDenomination, bob.ActiveOutputAddress.ScriptPubKey);
                        }

                        // 2.1 newDenomination may differs from the Denomination at registration, so we may not be able to tinker with
                        // additional outputs.
                        bool tinkerWithAdditionalMixingLevels = CanUseAdditionalOutputs(newDenomination);

                        if (tinkerWithAdditionalMixingLevels)
                        {
                            foreach (MixingLevel level in MixingLevels.GetLevelsExceptBase())
                            {
                                IEnumerable <Bob> bobsOnThisLevel = Bobs.Where(x => x.Level == level);
                                if (bobsOnThisLevel.Count() <= 1)
                                {
                                    break;
                                }

                                foreach (Bob bob in bobsOnThisLevel)
                                {
                                    transaction.Outputs.AddWithOptimize(level.Denomination, bob.ActiveOutputAddress.ScriptPubKey);
                                }
                            }
                        }

                        BitcoinWitPubKeyAddress coordinatorAddress = Constants.GetCoordinatorAddress(Network);
                        // 3. If there are less Bobs than Alices, then add our own address. The malicious Alice, who will refuse to sign.
                        for (int i = 0; i < MixingLevels.Count(); i++)
                        {
                            var aliceCountInLevel = Alices.Count(x => i < x.BlindedOutputScripts.Length);
                            var missingBobCount   = aliceCountInLevel - Bobs.Count(x => x.Level == MixingLevels.GetLevel(i));
                            for (int j = 0; j < missingBobCount; j++)
                            {
                                var denomination = MixingLevels.GetLevel(i).Denomination;
                                transaction.Outputs.AddWithOptimize(denomination, coordinatorAddress);
                            }
                        }

                        // 4. Start building Coordinator fee.
                        var   baseDenominationOutputCount = transaction.Outputs.Count(x => x.Value == newDenomination);
                        Money coordinatorBaseFeePerAlice  = newDenomination.Percentage(CoordinatorFeePercent * baseDenominationOutputCount);
                        Money coordinatorFee = baseDenominationOutputCount * coordinatorBaseFeePerAlice;

                        if (tinkerWithAdditionalMixingLevels)
                        {
                            foreach (MixingLevel level in MixingLevels.GetLevelsExceptBase())
                            {
                                var denominationOutputCount = transaction.Outputs.Count(x => x.Value == level.Denomination);
                                if (denominationOutputCount <= 1)
                                {
                                    break;
                                }

                                Money coordinatorLevelFeePerAlice = level.Denomination.Percentage(CoordinatorFeePercent * denominationOutputCount);
                                coordinatorFee += coordinatorLevelFeePerAlice * denominationOutputCount;
                            }
                        }

                        // 5. Add the inputs and the changes of Alices.
                        var spentCoins = new List <Coin>();
                        foreach (Alice alice in Alices)
                        {
                            foreach (var input in alice.Inputs)
                            {
                                transaction.Inputs.Add(new TxIn(input.Outpoint));
                                spentCoins.Add(input);
                            }

                            Money changeAmount = alice.InputSum - alice.NetworkFeeToPay - newDenomination - coordinatorBaseFeePerAlice;

                            if (tinkerWithAdditionalMixingLevels)
                            {
                                for (int i = 1; i < alice.BlindedOutputScripts.Length; i++)
                                {
                                    MixingLevel level = MixingLevels.GetLevel(i);
                                    var         denominationOutputCount = transaction.Outputs.Count(x => x.Value == level.Denomination);
                                    if (denominationOutputCount <= 1)
                                    {
                                        break;
                                    }

                                    changeAmount -= (level.Denomination + FeePerOutputs + (level.Denomination.Percentage(CoordinatorFeePercent * denominationOutputCount)));
                                }
                            }

                            if (changeAmount > Money.Zero)                                       // If the coordinator fee would make change amount to be negative or zero then no need to pay it.
                            {
                                Money minimumOutputAmount      = Money.Coins(0.0001m);           // If the change would be less than about $1 then add it to the coordinator.
                                Money onePercentOfDenomination = newDenomination.Percentage(1m); // If the change is less than about 1% of the newDenomination then add it to the coordinator fee.
                                Money minimumChangeAmount      = Math.Max(minimumOutputAmount, onePercentOfDenomination);
                                if (changeAmount < minimumChangeAmount)
                                {
                                    coordinatorFee += changeAmount;
                                }
                                else
                                {
                                    transaction.Outputs.AddWithOptimize(changeAmount, alice.ChangeOutputAddress.ScriptPubKey);
                                }
                            }
                            else
                            {
                                // Alice has no money enough to pay the coordinator fee then allow her to pay what she can.
                                coordinatorFee += changeAmount;
                            }
                        }

                        // 6. Add Coordinator fee only if > about $3, else just let it to be miner fee.
                        if (coordinatorFee > Money.Coins(0.0003m))
                        {
                            transaction.Outputs.AddWithOptimize(coordinatorFee, coordinatorAddress);
                        }

                        // 7. Create the unsigned transaction.
                        var builder = Network.CreateTransactionBuilder();
                        UnsignedCoinJoin = builder
                                           .ContinueToBuild(transaction)
                                           .AddCoins(spentCoins)              // It makes sure the UnsignedCoinJoin goes through TransactionBuilder optimizations.
                                           .BuildTransaction(false);

                        // 8. Try optimize fees.
                        await OptimizeFeesAsync(spentCoins);

                        SignedCoinJoin = Transaction.Parse(UnsignedCoinJoin.ToHex(), Network);

                        Phase = CcjRoundPhase.Signing;
                    }
                    else
                    {
                        return;
                    }

                    Logger.LogInfo <CcjRound>($"Round ({RoundId}): Phase initialized: {expectedPhase.ToString()}.");
                }