Ejemplo n.º 1
0
        public async Task <IEnumerable <Alice> > RemoveAlicesIfInputsSpentAsync()
        {
            var alicesRemoved = new List <Alice>();

            using (RoundSyncronizerLock.Lock())
            {
                if ((Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation) || Status != CcjRoundStatus.Running)
                {
                    throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
                }

                foreach (Alice alice in Alices)
                {
                    foreach (OutPoint input in alice.Inputs.Select(y => y.OutPoint))
                    {
                        GetTxOutResponse getTxOutResponse = await RpcClient.GetTxOutAsync(input.Hash, (int)input.N, includeMempool : true);

                        // Check if inputs are unspent.
                        if (getTxOutResponse == null)
                        {
                            alicesRemoved.Add(alice);
                            Alices.Remove(alice);
                        }
                    }
                }
            }

            foreach (var alice in alicesRemoved)
            {
                Logger.LogInfo <CcjRound>($"Round ({RoundId}): Alice ({alice.UniqueId}) removed.");
            }

            return(alicesRemoved);
        }
Ejemplo n.º 2
0
 public Alice TryGetAliceBy(Guid uniqueId)
 {
     using (RoundSyncronizerLock.Lock())
     {
         return(Alices.SingleOrDefault(x => x.UniqueId == uniqueId));
     }
 }
Ejemplo n.º 3
0
 public bool AllAlices(AliceState state)
 {
     using (RoundSyncronizerLock.Lock())
     {
         return(Alices.All(x => x.State == state));
     }
 }
Ejemplo n.º 4
0
 public IEnumerable <Alice> GetAlicesBy(AliceState state)
 {
     using (RoundSyncronizerLock.Lock())
     {
         return(Alices.Where(x => x.State == state).ToList());
     }
 }
Ejemplo n.º 5
0
 public bool TryRemoveAlice(Guid uniqueId)
 {
     lock (_aliceLock)
     {
         Alice alice = Alices.FirstOrDefault(x => x.UniqueId == uniqueId);
         return(Alices.TryRemove(alice));
     }
 }
Ejemplo n.º 6
0
 public IEnumerable <Alice> GetAlicesByNot(AliceState state, bool syncLock = true)
 {
     if (syncLock)
     {
         using (RoundSyncronizerLock.Lock())
         {
             return(Alices.Where(x => x.State != state).ToList());
         }
     }
     return(Alices.Where(x => x.State != state).ToList());
 }
Ejemplo n.º 7
0
        public void AddAlice(Alice alice)
        {
            using (RoundSyncronizerLock.Lock())
            {
                if (Phase != CcjRoundPhase.InputRegistration || Status != CcjRoundStatus.Running)
                {
                    throw new InvalidOperationException("Adding Alice is only allowed in InputRegistration phase.");
                }
                Alices.Add(alice);
            }

            StartAliceTimeout(alice.UniqueId);

            Logger.LogInfo <CcjRound>($"Round ({RoundId}): Alice ({alice.UniqueId}) added.");
        }
Ejemplo n.º 8
0
        public void StartAliceTimeout(Guid uniqueId)
        {
            // 1. Find Alice and set its LastSeen propery.
            var foundAlice = false;
            var started    = DateTimeOffset.UtcNow;

            using (RoundSyncronizerLock.Lock())
            {
                if (Phase != CcjRoundPhase.InputRegistration || Status != CcjRoundStatus.Running)
                {
                    return;                     // Then no need to timeout alice.
                }

                Alice alice = Alices.SingleOrDefault(x => x.UniqueId == uniqueId);
                foundAlice = alice != default(Alice);
                if (foundAlice)
                {
                    alice.LastSeen = started;
                }
            }

            if (foundAlice)
            {
                Task.Run(async() =>
                {
                    // 2. Delay asyncronously to the requested timeout
                    await Task.Delay(AliceRegistrationTimeout);

                    using (await RoundSyncronizerLock.LockAsync())
                    {
                        // 3. If the round is still running and the phase is still InputRegistration
                        if (Status == CcjRoundStatus.Running && Phase == CcjRoundPhase.InputRegistration)
                        {
                            Alice alice = Alices.SingleOrDefault(x => x.UniqueId == uniqueId);
                            if (alice != default(Alice))
                            {
                                // 4. If LastSeen isn't changed by then, remove Alice.
                                if (alice.LastSeen == started)
                                {
                                    Alices.Remove(alice);
                                    Logger.LogInfo <CcjRound>($"Round ({RoundId}): Alice ({alice.UniqueId}) timed out.");
                                }
                            }
                        }
                    }
                });
            }
        }
Ejemplo n.º 9
0
        public Alice FindAlice(string uniqueId, bool throwException)
        {
            lock (_aliceLock)
            {
                Alice alice = Alices.FirstOrDefault(x => x.UniqueId == new Guid(uniqueId));
                if (alice == default(Alice))
                {
                    if (throwException)
                    {
                        throw new ArgumentException("Wrong uniqueId");
                    }
                }

                return(alice);
            }
        }
Ejemplo n.º 10
0
        public int RemoveAliceIfContains(OutPoint input)
        {
            var numberOfRemovedAlices = 0;

            using (RoundSyncronizerLock.Lock())
            {
                if ((Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation) || Status != CcjRoundStatus.Running)
                {
                    throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
                }
                numberOfRemovedAlices = Alices.RemoveAll(x => x.Inputs.Any(y => y.OutPoint == input));
            }

            Logger.LogInfo <CcjRound>($"Round ({RoundId}): {numberOfRemovedAlices} alices are removed.");

            return(numberOfRemovedAlices);
        }
Ejemplo n.º 11
0
        public int RemoveAlicesBy(AliceState state)
        {
            int numberOfRemovedAlices = 0;

            using (RoundSyncronizerLock.Lock())
            {
                if ((Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation) || Status != CcjRoundStatus.Running)
                {
                    throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
                }
                numberOfRemovedAlices = Alices.RemoveAll(x => x.State == state);
            }
            if (numberOfRemovedAlices != 0)
            {
                Logger.LogInfo <CcjRound>($"Round ({RoundId}): {numberOfRemovedAlices} alices in {state} state are removed.");
            }
            return(numberOfRemovedAlices);
        }
Ejemplo n.º 12
0
        public int RemoveAlicesBy(params Guid[] ids)
        {
            var numberOfRemovedAlices = 0;

            using (RoundSyncronizerLock.Lock())
            {
                if ((Phase != CcjRoundPhase.InputRegistration && Phase != CcjRoundPhase.ConnectionConfirmation) || Status != CcjRoundStatus.Running)
                {
                    throw new InvalidOperationException("Removing Alice is only allowed in InputRegistration and ConnectionConfirmation phases.");
                }
                foreach (var id in ids)
                {
                    numberOfRemovedAlices = Alices.RemoveAll(x => x.UniqueId == id);
                }
            }

            Logger.LogInfo <CcjRound>($"Round ({RoundId}): {numberOfRemovedAlices} alices are removed.");

            return(numberOfRemovedAlices);
        }
Ejemplo n.º 13
0
        public async Task BroadcastCoinJoinIfFullySignedAsync()
        {
            using (await RoundSyncronizerLock.LockAsync())
            {
                // Check if fully signed.
                if (SignedCoinJoin.Inputs.All(x => x.HasWitness()))
                {
                    Logger.LogInfo <CcjRound>($"Round ({RoundId}): Trying to broadcast coinjoin.");

                    try
                    {
                        Coin[] spentCoins = Alices.SelectMany(x => x.Inputs.Select(y => new Coin(y.OutPoint, y.Output))).ToArray();
                        Money  networkFee = SignedCoinJoin.GetFee(spentCoins);
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): Network Fee: {networkFee.ToString(false, false)} BTC.");
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): Coordinator Fee: {SignedCoinJoin.Outputs.SingleOrDefault(x => x.ScriptPubKey == Constants.GetCoordinatorAddress(RpcClient.Network).ScriptPubKey)?.Value?.ToString(false, false) ?? "0"} BTC.");
                        FeeRate feeRate = SignedCoinJoin.GetFeeRate(spentCoins);
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): Network Fee Rate: {feeRate.FeePerK.ToDecimal(MoneyUnit.Satoshi) / 1000} satoshi/byte.");
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): Number of inputs: {SignedCoinJoin.Inputs.Count}.");
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): Number of outputs: {SignedCoinJoin.Outputs.Count}.");
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): Serialized Size: {SignedCoinJoin.GetSerializedSize() / 1024} KB.");
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): VSize: {SignedCoinJoin.GetVirtualSize() / 1024} KB.");
                        foreach (var o in SignedCoinJoin.GetIndistinguishableOutputs().Where(x => x.count > 1))
                        {
                            Logger.LogInfo <CcjRound>($"Round ({RoundId}): There are {o.count} occurences of {o.value.ToString(true, false)} BTC output.");
                        }

                        await RpcClient.SendRawTransactionAsync(SignedCoinJoin);

                        Succeed(syncLock: false);
                        Logger.LogInfo <CcjRound>($"Round ({RoundId}): Successfully broadcasted the CoinJoin: {SignedCoinJoin.GetHash()}.");
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError <CcjRound>($"Round ({RoundId}): Failed to broadcast the CoinJoin: {SignedCoinJoin.GetHash()}.");
                        Logger.LogError <CcjRound>(ex);
                        Fail(syncLock: false);
                    }
                }
            }
        }
Ejemplo n.º 14
0
        public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase)
        {
            using (await RoundSyncronizerLock.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
                        var inputSizeInBytes  = (int)Math.Ceiling(((3 * Constants.P2wpkhInputSizeInBytes) + Constants.P2pkhInputSizeInBytes) / 4m);
                        var outputSizeInBytes = Constants.OutputSizeInBytes;
                        try
                        {
                            var estimateSmartFeeResponse = await RpcClient.EstimateSmartFeeAsync(ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest : true);

                            if (estimateSmartFeeResponse == null)
                            {
                                throw new InvalidOperationException("FeeRate is not yet initialized");
                            }
                            var   feeRate     = estimateSmartFeeResponse.FeeRate;
                            Money feePerBytes = (feeRate.FeePerK / 1000);

                            // Make sure min relay fee (1000 sat) is hit.
                            FeePerInputs  = Math.Max(feePerBytes * inputSizeInBytes, new Money(500));
                            FeePerOutputs = Math.Max(feePerBytes * outputSizeInBytes, new Money(250));
                        }
                        catch (Exception ex)
                        {
                            // If fee hasn't been initialized once, fall back.
                            if (FeePerInputs == null || FeePerOutputs == null)
                            {
                                var feePerBytes = new Money(100);                                 // 100 satoshi per byte

                                // Make sure min relay fee (1000 sat) is hit.
                                FeePerInputs  = Math.Max(feePerBytes * inputSizeInBytes, new Money(500));
                                FeePerOutputs = Math.Max(feePerBytes * outputSizeInBytes, new Money(250));
                            }

                            Logger.LogError <CcjRound>(ex);
                        }

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

                        RoundHash = NBitcoinHelpers.HashOutpoints(Alices.SelectMany(x => x.Inputs).Select(y => y.OutPoint));

                        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

                        // 1. Set new denomination: minor optimization.
                        Money newDenomination = Alices.Min(x => x.OutputSumWithoutCoordinatorFeeAndDenomination);
                        var   transaction     = new Transaction();

                        // 2. Add Bob outputs.
                        foreach (Bob bob in Bobs)
                        {
                            transaction.AddOutput(newDenomination, bob.ActiveOutputAddress.ScriptPubKey);
                        }

                        BitcoinWitPubKeyAddress coordinatorAddress = Constants.GetCoordinatorAddress(RpcClient.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 < Alices.Count - Bobs.Count; i++)
                        {
                            transaction.AddOutput(newDenomination, coordinatorAddress);
                        }

                        // 4. Start building Coordinator fee.
                        Money coordinatorFeePerAlice = newDenomination.Percentange(CoordinatorFeePercent);
                        Money coordinatorFee         = Alices.Count * coordinatorFeePerAlice;

                        // 5. Add the inputs and the changes of Alices.
                        foreach (Alice alice in Alices)
                        {
                            foreach (var input in alice.Inputs)
                            {
                                transaction.AddInput(new TxIn(input.OutPoint));
                            }
                            Money changeAmount = alice.GetChangeAmount(newDenomination, coordinatorFeePerAlice);
                            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.Percentange(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.AddOutput(changeAmount, alice.ChangeOutputAddress.ScriptPubKey);
                                }
                            }
                            else
                            {
                                coordinatorFee -= coordinatorFeePerAlice;
                            }
                        }

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

                        // 7. Create the unsigned transaction.
                        var builder = new TransactionBuilder();
                        UnsignedCoinJoin = builder
                                           .ContinueToBuild(transaction)
                                           .Shuffle()
                                           .BuildTransaction(false);

                        SignedCoinJoin = new Transaction(UnsignedCoinJoin.ToHex());

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

                    Logger.LogInfo <CcjRound>($"Round ({RoundId}): Phase initialized: {expectedPhase.ToString()}.");
                }
                catch (Exception ex)
                {
                    Logger.LogError <CcjRound>(ex);
                    Status = CcjRoundStatus.Failed;
                    throw;
                }
            }

#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
            Task.Run(async() =>
            {
                TimeSpan timeout;
                switch (expectedPhase)
                {
                case CcjRoundPhase.InputRegistration:
                    timeout = InputRegistrationTimeout;
                    break;

                case CcjRoundPhase.ConnectionConfirmation:
                    timeout = ConnectionConfirmationTimeout;
                    break;

                case CcjRoundPhase.OutputRegistration:
                    timeout = OutputRegistrationTimeout;
                    break;

                case CcjRoundPhase.Signing:
                    timeout = SigningTimeout;
                    break;

                default: throw new InvalidOperationException("This is impossible to happen.");
                }

                // Delay asyncronously to the requested timeout.
                await Task.Delay(timeout);

                var executeRunFailure = false;
                using (await RoundSyncronizerLock.LockAsync())
                {
                    executeRunFailure = Status == CcjRoundStatus.Running && Phase == expectedPhase;
                }
                if (executeRunFailure)
                {
                    Logger.LogInfo <CcjRound>($"Round ({RoundId}): {expectedPhase.ToString()} timed out after {timeout.TotalSeconds} seconds. Failure mode is executing.");

                    // This will happen outside the lock.
                    Task.Run(async() =>
                    {
                        try
                        {
                            switch (expectedPhase)
                            {
                            case CcjRoundPhase.InputRegistration:
                                {
                                    // Only fail if less two one Alice is registered.
                                    // Don't ban anyone, it's ok if they lost connection.
                                    await RemoveAlicesIfInputsSpentAsync();
                                    int aliceCountAfterInputRegistrationTimeout = CountAlices();
                                    if (aliceCountAfterInputRegistrationTimeout < 2)
                                    {
                                        Fail();
                                    }
                                    else
                                    {
                                        UpdateAnonymitySet(aliceCountAfterInputRegistrationTimeout);
                                        // Progress to the next phase, which will be ConnectionConfirmation
                                        await ExecuteNextPhaseAsync(CcjRoundPhase.ConnectionConfirmation);
                                    }
                                }
                                break;

                            case CcjRoundPhase.ConnectionConfirmation:
                                {
                                    // Only fail if less than two one alices are registered.
                                    // What if an attacker registers all the time many alices, then drops out. He'll achieve only 2 alices to participate?
                                    // If he registers many alices at InputRegistration
                                    // AND never confirms in connection confirmation
                                    // THEN connection confirmation will go with 2 alices in every round
                                    // Therefore Alices those didn't confirm, nor requested dsconnection should be banned:
                                    IEnumerable <Alice> alicesToBan1 = GetAlicesBy(AliceState.InputsRegistered);
                                    IEnumerable <Alice> alicesToBan2 = await RemoveAlicesIfInputsSpentAsync();                                            // So ban only those who confirmed participation, yet spent their inputs.

                                    IEnumerable <OutPoint> inputsToBan = alicesToBan1.SelectMany(x => x.Inputs).Select(y => y.OutPoint).Concat(alicesToBan2.SelectMany(x => x.Inputs).Select(y => y.OutPoint).ToArray()).Distinct();

                                    if (inputsToBan.Any())
                                    {
                                        await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, inputsToBan.ToArray());
                                    }

                                    RemoveAlicesBy(alicesToBan1.Select(x => x.UniqueId).Concat(alicesToBan2.Select(y => y.UniqueId)).Distinct().ToArray());

                                    int aliceCountAfterConnectionConfirmationTimeout = CountAlices();
                                    if (aliceCountAfterConnectionConfirmationTimeout < 2)
                                    {
                                        Fail();
                                    }
                                    else
                                    {
                                        UpdateAnonymitySet(aliceCountAfterConnectionConfirmationTimeout);
                                        // Progress to the next phase, which will be OutputRegistration
                                        await ExecuteNextPhaseAsync(CcjRoundPhase.OutputRegistration);
                                    }
                                }
                                break;

                            case CcjRoundPhase.OutputRegistration:
                                {
                                    // Output registration never fails.
                                    // We don't know which Alice to ban.
                                    // Therefore proceed to signing, and whichever Alice doesn't sign ban.
                                    await ExecuteNextPhaseAsync(CcjRoundPhase.Signing);
                                }
                                break;

                            case CcjRoundPhase.Signing:
                                {
                                    var outpointsToBan = new List <OutPoint>();
                                    using (await RoundSyncronizerLock.LockAsync())
                                    {
                                        foreach (Alice alice in Alices)
                                        {
                                            if (alice.State != AliceState.SignedCoinJoin)
                                            {
                                                outpointsToBan.AddRange(alice.Inputs.Select(x => x.OutPoint));
                                            }
                                        }
                                    }
                                    if (outpointsToBan.Any())
                                    {
                                        await UtxoReferee.BanUtxosAsync(1, DateTimeOffset.UtcNow, outpointsToBan.ToArray());
                                    }
                                    Fail();
                                }
                                break;

                            default: throw new InvalidOperationException("This is impossible to happen.");
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogWarning <CcjRound>($"Round ({RoundId}): {expectedPhase.ToString()} timeout failed with exception: {ex}");
                        }
                    });
                }
            });
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
        }
Ejemplo n.º 15
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()}.");
                }
Ejemplo n.º 16
0
        public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase)
        {
            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
                        var inputSizeInBytes  = (int)Math.Ceiling(((3 * Constants.P2wpkhInputSizeInBytes) + Constants.P2pkhInputSizeInBytes) / 4m);
                        var outputSizeInBytes = Constants.OutputSizeInBytes;
                        try
                        {
                            var estimateSmartFeeResponse = await RpcClient.EstimateSmartFeeAsync(ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest : true, tryOtherFeeRates : true);

                            if (estimateSmartFeeResponse is null)
                            {
                                throw new InvalidOperationException("FeeRate is not yet initialized");
                            }
                            var   feeRate     = estimateSmartFeeResponse.FeeRate;
                            Money feePerBytes = (feeRate.FeePerK / 1000);

                            // Make sure min relay fee (1000 sat) is hit.
                            FeePerInputs  = Math.Max(feePerBytes * inputSizeInBytes, new Money(500));
                            FeePerOutputs = Math.Max(feePerBytes * outputSizeInBytes, new Money(250));
                        }
                        catch (Exception ex)
                        {
                            // If fee hasn't been initialized once, fall back.
                            if (FeePerInputs is null || FeePerOutputs is null)
                            {
                                var feePerBytes = new Money(100);                                 // 100 satoshi per byte

                                // Make sure min relay fee (1000 sat) is hit.
                                FeePerInputs  = Math.Max(feePerBytes * inputSizeInBytes, new Money(500));
                                FeePerOutputs = Math.Max(feePerBytes * outputSizeInBytes, new Money(250));
                            }

                            Logger.LogError <CcjRound>(ex);
                        }

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

                        RoundHash = NBitcoinHelpers.HashOutpoints(Alices.SelectMany(x => x.Inputs).Select(y => y.Outpoint));

                        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

                        // 1. Set new denomination: minor optimization.
                        Money newDenomination = Alices.Min(x => x.OutputSumWithoutCoordinatorFeeAndDenomination);
                        var   transaction     = Network.Consensus.ConsensusFactory.CreateTransaction();

                        // 2. Add Bob outputs.
                        foreach (Bob bob in Bobs)
                        {
                            transaction.Outputs.Add(newDenomination, 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 < Alices.Count - Bobs.Count; i++)
                        {
                            transaction.Outputs.Add(newDenomination, coordinatorAddress);
                        }

                        // 4. Start building Coordinator fee.
                        Money coordinatorFeePerAlice = newDenomination.Percentange(CoordinatorFeePercent) * Alices.Count;
                        Money coordinatorFee         = Alices.Count * coordinatorFeePerAlice;

                        // 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.GetChangeAmount(newDenomination, coordinatorFeePerAlice);
                            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.Percentange(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.Add(changeAmount, alice.ChangeOutputAddress.ScriptPubKey);
                                }
                            }
                            else
                            {
                                coordinatorFee -= coordinatorFeePerAlice;
                            }
                        }

                        // 6. Add Coordinator fee only if > about $3, else just let it to be miner fee.
                        if (coordinatorFee > Money.Coins(0.0003m))
                        {
                            transaction.Outputs.Add(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.
                        try
                        {
                            // 8.1. Estimate the current FeeRate. Note, there are no signatures yet!
                            int   estimatedSigSizeBytes = UnsignedCoinJoin.Inputs.Count * Constants.P2wpkhInputSizeInBytes;
                            int   estimatedFinalTxSize  = UnsignedCoinJoin.GetSerializedSize() + estimatedSigSizeBytes;
                            Money fee = UnsignedCoinJoin.GetFee(spentCoins.ToArray());
                            // There is a currentFeeRate null check later.
                            FeeRate currentFeeRate = fee is null ? null : new FeeRate(fee, estimatedFinalTxSize);

                            // 8.2. Get the most optimal FeeRate.
                            EstimateSmartFeeResponse estimateSmartFeeResponse = await RpcClient.EstimateSmartFeeAsync(ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest : true, tryOtherFeeRates : true);

                            if (estimateSmartFeeResponse is null)
                            {
                                throw new InvalidOperationException("FeeRate is not yet initialized");
                            }
                            FeeRate optimalFeeRate = estimateSmartFeeResponse.FeeRate;

                            if (!(optimalFeeRate is null) && optimalFeeRate != FeeRate.Zero && !(currentFeeRate is null) && currentFeeRate != FeeRate.Zero) // This would be really strange if it'd happen.
                            {
                                var sanityFeeRate = new FeeRate(2m);                                                                                        // 2 s/b
                                optimalFeeRate = optimalFeeRate < sanityFeeRate ? sanityFeeRate : optimalFeeRate;
                                if (optimalFeeRate < currentFeeRate)
                                {
                                    // 8.2 If the fee can be lowered, lower it.
                                    // 8.2.1. How much fee can we save?
                                    Money feeShouldBePaid = new Money(estimatedFinalTxSize * (int)optimalFeeRate.SatoshiPerByte);
                                    Money toSave          = fee - feeShouldBePaid;

                                    // 8.2.2. Get the outputs to divide  the savings between.
                                    int   maxMixCount   = UnsignedCoinJoin.GetIndistinguishableOutputs().Max(x => x.count);
                                    Money bestMixAmount = UnsignedCoinJoin.GetIndistinguishableOutputs().Where(x => x.count == maxMixCount).Max(x => x.value);
                                    int   bestMixCount  = UnsignedCoinJoin.GetIndistinguishableOutputs().First(x => x.value == bestMixAmount).count;

                                    // 8.2.3. Get the savings per best mix outputs.
                                    long toSavePerBestMixOutputs = toSave.Satoshi / bestMixCount;

                                    // 8.2.4. Modify the best mix outputs in the transaction.
                                    if (toSavePerBestMixOutputs > 0)
                                    {
                                        foreach (TxOut output in UnsignedCoinJoin.Outputs.Where(x => x.Value == bestMixAmount))
                                        {
                                            output.Value += toSavePerBestMixOutputs;
                                        }
                                    }
                                }
                            }
                            else
                            {
                                Logger.LogError <CcjRound>($"This is impossible. {nameof(optimalFeeRate)}: {optimalFeeRate}, {nameof(currentFeeRate)}: {currentFeeRate}.");
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogWarning <CcjRound>("Couldn't optimize fees. Fallback to normal fees.");
                            Logger.LogWarning <CcjRound>(ex);
                        }

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

                        Phase = CcjRoundPhase.Signing;
                    }