private async Task <RoundState> WaitForBlameRoundAsync(uint256 blameRoundId, CancellationToken token) { var timeout = TimeSpan.FromMinutes(5); CoinJoinClientProgress.SafeInvoke(this, new WaitingForBlameRound(DateTimeOffset.UtcNow + timeout)); using CancellationTokenSource waitForBlameRoundCts = new(timeout); using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(waitForBlameRoundCts.Token, token); var roundState = await RoundStatusUpdater .CreateRoundAwaiter( roundState => roundState.BlameOf == blameRoundId, linkedCts.Token) .ConfigureAwait(false); if (roundState.Phase is not Phase.InputRegistration) { throw new InvalidOperationException($"Blame Round ({roundState.Id}): Abandoning: the round is not in Input Registration but in '{roundState.Phase}'."); } if (roundState.CoinjoinState.Parameters.AllowedOutputAmounts.Min >= MinimumOutputAmountSanity) { throw new InvalidOperationException($"Blame Round ({roundState.Id}): Abandoning: the minimum output amount is too high."); } if (!IsRoundEconomic(roundState.CoinjoinState.Parameters.MiningFeeRate)) { throw new InvalidOperationException($"Blame Round ({roundState.Id}): Abandoning: the round is not economic."); } return(roundState); }
private async Task <RoundState> WaitForRoundAsync(uint256 excludeRound, CancellationToken token) { CoinJoinClientProgress.SafeInvoke(this, new WaitingForRound()); return(await RoundStatusUpdater .CreateRoundAwaiter( roundState => roundState.InputRegistrationEnd - DateTimeOffset.UtcNow > DoNotRegisterInLastMinuteTimeLimit && roundState.CoinjoinState.Parameters.AllowedOutputAmounts.Min < MinimumOutputAmountSanity && roundState.Phase == Phase.InputRegistration && roundState.BlameOf == uint256.Zero && IsRoundEconomic(roundState.CoinjoinState.Parameters.MiningFeeRate) && roundState.Id != excludeRound, token) .ConfigureAwait(false)); }
ProceedWithSigningStateAsync(uint256 roundId, ImmutableArray <AliceClient> registeredAliceClients, IEnumerable <TxOut> outputTxOuts, CancellationToken cancellationToken) { // Signing. var roundState = await RoundStatusUpdater.CreateRoundAwaiter(roundId, Phase.TransactionSigning, cancellationToken).ConfigureAwait(false); var remainingTime = roundState.CoinjoinState.Parameters.TransactionSigningTimeout - RoundStatusUpdater.Period; var signingStateEndTime = DateTimeOffset.UtcNow + remainingTime; CoinJoinClientProgress.SafeInvoke(this, new EnteringSigningPhase(roundState, signingStateEndTime)); using CancellationTokenSource phaseTimeoutCts = new(remainingTime + ExtraPhaseTimeoutMargin); using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, phaseTimeoutCts.Token); roundState.LogDebug($"Transaction signing phase started - it will end in: {signingStateEndTime - DateTimeOffset.UtcNow:hh\\:mm\\:ss}."); var signingState = roundState.Assert <SigningState>(); var unsignedCoinJoin = signingState.CreateUnsignedTransaction(); // If everything is okay, then sign all the inputs. Otherwise, in case there are missing outputs, the server is // lying (it lied us before when it responded with 200 OK to the OutputRegistration requests or it is lying us // now when we identify as satoshi. // In this scenario we should ban the coordinator and stop dealing with it. // see more: https://github.com/zkSNACKs/WalletWasabi/issues/8171 bool mustSignAllInputs = SanityCheck(outputTxOuts, unsignedCoinJoin); if (!mustSignAllInputs) { roundState.LogInfo($"There are missing outputs. A subset of inputs will be signed."); } // Send signature. var combinedToken = linkedCts.Token; var alicesToSign = mustSignAllInputs ? registeredAliceClients : registeredAliceClients.RemoveAt(SecureRandom.GetInt(0, registeredAliceClients.Length)); await SignTransactionAsync(alicesToSign, unsignedCoinJoin, signingStateEndTime, combinedToken).ConfigureAwait(false); roundState.LogDebug($"{alicesToSign.Length} out of {registeredAliceClients.Length} Alices have signed the coinjoin tx."); return(unsignedCoinJoin, alicesToSign); }
private async Task <IEnumerable <TxOut> > ProceedWithOutputRegistrationPhaseAsync(uint256 roundId, ImmutableArray <AliceClient> registeredAliceClients, CancellationToken cancellationToken) { // Waiting for OutputRegistration phase, all the Alices confirmed their connections, so the list of the inputs will be complete. var roundState = await RoundStatusUpdater.CreateRoundAwaiter(roundId, Phase.OutputRegistration, cancellationToken).ConfigureAwait(false); var roundParameters = roundState.CoinjoinState.Parameters; var remainingTime = roundParameters.OutputRegistrationTimeout - RoundStatusUpdater.Period; var now = DateTimeOffset.UtcNow; var outputRegistrationPhaseEndTime = now + remainingTime; // Splitting the remaining time. // Both operations are done under output registration phase, so we have to do the random timing taking that into account. var outputRegistrationEndTime = now + (remainingTime * 0.8); // 80% of the time. var readyToSignEndTime = outputRegistrationEndTime + remainingTime * 0.2; // 20% of the time. CoinJoinClientProgress.SafeInvoke(this, new EnteringOutputRegistrationPhase(roundState, outputRegistrationPhaseEndTime)); using CancellationTokenSource phaseTimeoutCts = new(remainingTime + ExtraPhaseTimeoutMargin); using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, phaseTimeoutCts.Token); var registeredCoins = registeredAliceClients.Select(x => x.SmartCoin.Coin); var inputEffectiveValuesAndSizes = registeredAliceClients.Select(x => (x.EffectiveValue, x.SmartCoin.ScriptPubKey.EstimateInputVsize())); var availableVsize = registeredAliceClients.SelectMany(x => x.IssuedVsizeCredentials).Sum(x => x.Value); // Calculate outputs values var constructionState = roundState.Assert <ConstructionState>(); AmountDecomposer amountDecomposer = new(roundParameters.MiningFeeRate, roundParameters.AllowedOutputAmounts, Constants.P2wpkhOutputVirtualSize, Constants.P2wpkhInputVirtualSize, (int)availableVsize); var theirCoins = constructionState.Inputs.Where(x => !registeredCoins.Any(y => x.Outpoint == y.Outpoint)); var registeredCoinEffectiveValues = registeredAliceClients.Select(x => x.EffectiveValue); var theirCoinEffectiveValues = theirCoins.Select(x => x.EffectiveValue(roundParameters.MiningFeeRate, roundParameters.CoordinationFeeRate)); var outputValues = amountDecomposer.Decompose(registeredCoinEffectiveValues, theirCoinEffectiveValues); // Get as many destinations as outputs we need. var destinations = DestinationProvider.GetNextDestinations(outputValues.Count()).ToArray(); var outputTxOuts = outputValues.Zip(destinations, (amount, destination) => new TxOut(amount, destination.ScriptPubKey)); DependencyGraph dependencyGraph = DependencyGraph.ResolveCredentialDependencies(inputEffectiveValuesAndSizes, outputTxOuts, roundParameters.MiningFeeRate, roundParameters.CoordinationFeeRate, roundParameters.MaxVsizeAllocationPerAlice); DependencyGraphTaskScheduler scheduler = new(dependencyGraph); // Re-issuances. var bobClient = CreateBobClient(roundState); roundState.LogInfo("Starting reissuances."); var combinedToken = linkedCts.Token; await scheduler.StartReissuancesAsync(registeredAliceClients, bobClient, combinedToken).ConfigureAwait(false); // Output registration. roundState.LogDebug($"Output registration started - it will end in: {outputRegistrationEndTime - DateTimeOffset.UtcNow:hh\\:mm\\:ss}."); var outputRegistrationScheduledDates = GetScheduledDates(outputTxOuts.Count(), outputRegistrationEndTime, MaximumRequestDelay); await scheduler.StartOutputRegistrationsAsync(outputTxOuts, bobClient, KeyChain, outputRegistrationScheduledDates, combinedToken).ConfigureAwait(false); roundState.LogDebug($"Outputs({outputTxOuts.Count()}) were registered."); // ReadyToSign. roundState.LogDebug($"ReadyToSign phase started - it will end in: {readyToSignEndTime - DateTimeOffset.UtcNow:hh\\:mm\\:ss}."); await ReadyToSignAsync(registeredAliceClients, readyToSignEndTime, combinedToken).ConfigureAwait(false); roundState.LogDebug($"Alices({registeredAliceClients.Length}) are ready to sign."); return(outputTxOuts); }
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)); } }