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()}."); }
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 }
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; }