private void LogCoinJoinSummary(ImmutableArray <AliceClient> registeredAliceClients, IEnumerable <TxOut> myOutputs, Transaction unsignedCoinJoinTransaction, RoundState roundState) { RoundParameters roundParameters = roundState.CoinjoinState.Parameters; FeeRate feeRate = roundParameters.MiningFeeRate; var totalEffectiveInputAmount = Money.Satoshis(registeredAliceClients.Sum(a => a.EffectiveValue)); var totalEffectiveOutputAmount = Money.Satoshis(myOutputs.Sum(a => a.Value - feeRate.GetFee(a.ScriptPubKey.EstimateOutputVsize()))); var effectiveDifference = totalEffectiveInputAmount - totalEffectiveOutputAmount; var totalInputAmount = Money.Satoshis(registeredAliceClients.Sum(a => a.SmartCoin.Amount)); var totalOutputAmount = Money.Satoshis(myOutputs.Sum(a => a.Value)); var totalDifference = Money.Satoshis(totalInputAmount - totalOutputAmount); var inputNetworkFee = Money.Satoshis(registeredAliceClients.Sum(alice => feeRate.GetFee(alice.SmartCoin.Coin.ScriptPubKey.EstimateInputVsize()))); var outputNetworkFee = Money.Satoshis(myOutputs.Sum(output => feeRate.GetFee(output.ScriptPubKey.EstimateOutputVsize()))); var totalNetworkFee = inputNetworkFee + outputNetworkFee; var totalCoordinationFee = Money.Satoshis(registeredAliceClients.Where(a => a.IsPayingZeroCoordinationFee).Sum(a => roundParameters.CoordinationFeeRate.GetFee(a.SmartCoin.Amount))); string[] summary = new string[] { "", $"\tInput total: {totalInputAmount.ToString(true, false)} Eff: {totalEffectiveInputAmount.ToString(true, false)} NetwFee: {inputNetworkFee.ToString(true, false)} CoordFee: {totalCoordinationFee.ToString(true)}", $"\tOutpu total: {totalOutputAmount.ToString(true, false)} Eff: {totalEffectiveOutputAmount.ToString(true, false)} NetwFee: {outputNetworkFee.ToString(true, false)}", $"\tTotal diff : {totalDifference.ToString(true, false)}", $"\tEffec diff : {effectiveDifference.ToString(true, false)}", $"\tTotal fee : {totalNetworkFee.ToString(true, false)}" }; roundState.LogDebug(string.Join(Environment.NewLine, summary)); }
public async Task <CoinJoinResult> StartRoundAsync(IEnumerable <SmartCoin> smartCoins, RoundState roundState, CancellationToken cancellationToken) { var roundId = roundState.Id; ImmutableArray <(AliceClient AliceClient, PersonCircuit PersonCircuit)> registeredAliceClientAndCircuits = ImmutableArray <(AliceClient, PersonCircuit)> .Empty; // Because of the nature of the protocol, the input registration and the connection confirmation phases are done subsequently thus they have a merged timeout. var timeUntilOutputReg = (roundState.InputRegistrationEnd - DateTimeOffset.UtcNow) + roundState.CoinjoinState.Parameters.ConnectionConfirmationTimeout; try { try { using CancellationTokenSource timeUntilOutputRegCts = new(timeUntilOutputReg + ExtraPhaseTimeoutMargin); using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeUntilOutputRegCts.Token); registeredAliceClientAndCircuits = await ProceedWithInputRegAndConfirmAsync(smartCoins, roundState, linkedCts.Token).ConfigureAwait(false); } catch (UnexpectedRoundPhaseException ex) { roundState = ex.RoundState; var message = ex.RoundState.EndRoundState switch { EndRoundState.AbortedNotEnoughAlices => $"Not enough participants in the round to continue. Waiting for a new round.", _ => $"Registration phase ended by the coordinator: '{ex.Message}' code: '{ex.RoundState.EndRoundState}'." }; roundState.LogInfo(message); return(new CoinJoinResult(false)); } if (!registeredAliceClientAndCircuits.Any()) { roundState.LogInfo("There are no available Alices to participate with."); return(new CoinJoinResult(false)); } roundState.LogDebug($"Successfully registered {registeredAliceClientAndCircuits.Length} inputs."); var registeredAliceClients = registeredAliceClientAndCircuits.Select(x => x.AliceClient).ToImmutableArray(); var outputTxOuts = await ProceedWithOutputRegistrationPhaseAsync(roundId, registeredAliceClients, cancellationToken).ConfigureAwait(false); var(unsignedCoinJoin, aliceClientsThatSigned) = await ProceedWithSigningStateAsync(roundId, registeredAliceClients, outputTxOuts, cancellationToken).ConfigureAwait(false); roundState = await RoundStatusUpdater.CreateRoundAwaiter(s => s.Id == roundId && s.Phase == Phase.Ended, cancellationToken).ConfigureAwait(false); var msg = roundState.EndRoundState switch { EndRoundState.TransactionBroadcasted => $"Broadcasted. Coinjoin TxId: ({unsignedCoinJoin.GetHash()})", EndRoundState.TransactionBroadcastFailed => $"Failed to broadcast. Coinjoin TxId: ({unsignedCoinJoin.GetHash()})", EndRoundState.AbortedWithError => "Round abnormally finished.", EndRoundState.AbortedNotEnoughAlices => "Aborted. Not enough participants.", EndRoundState.AbortedNotEnoughAlicesSigned => "Aborted. Not enough participants signed the coinjoin transaction.", EndRoundState.NotAllAlicesSign => "Aborted. Some Alices didn't sign. Go to blame round.", EndRoundState.None => "Unknown.", _ => throw new ArgumentOutOfRangeException() }; roundState.LogInfo(msg); LogCoinJoinSummary(registeredAliceClients, outputTxOuts, unsignedCoinJoin, roundState); return(new CoinJoinResult( GoForBlameRound: roundState.EndRoundState == EndRoundState.NotAllAlicesSign, SuccessfulBroadcast: roundState.EndRoundState == EndRoundState.TransactionBroadcasted, RegisteredCoins: aliceClientsThatSigned.Select(a => a.SmartCoin).ToImmutableList(), RegisteredOutputs: outputTxOuts.Select(o => o.ScriptPubKey).ToImmutableList())); } finally { foreach (var coins in smartCoins) { coins.CoinJoinInProgress = false; } foreach (var aliceClientAndCircuit in registeredAliceClientAndCircuits) { aliceClientAndCircuit.AliceClient.Finish(); aliceClientAndCircuit.PersonCircuit.Dispose(); } CoinJoinClientProgress.SafeInvoke(this, new LeavingCriticalPhase()); CoinJoinClientProgress.SafeInvoke(this, new RoundEnded(roundState)); } }