Beispiel #1
0
    private async Task <ImmutableArray <(AliceClient, PersonCircuit)> > ProceedWithInputRegAndConfirmAsync(IEnumerable <SmartCoin> smartCoins, RoundState roundState, CancellationToken cancellationToken)
    {
        var remainingTime = roundState.InputRegistrationEnd - DateTimeOffset.UtcNow;

        using CancellationTokenSource phaseTimeoutCts = new(remainingTime + ExtraPhaseTimeoutMargin);
        using CancellationTokenSource linkedCts       = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, phaseTimeoutCts.Token);
        var combinedToken = linkedCts.Token;

        CoinJoinClientProgress.SafeInvoke(this, new EnteringInputRegistrationPhase(roundState, roundState.InputRegistrationEnd));

        // Register coins.
        var result = await CreateRegisterAndConfirmCoinsAsync(smartCoins, roundState, combinedToken).ConfigureAwait(false);

        if (!RoundStatusUpdater.TryGetRoundState(roundState.Id, out var newRoundState))
        {
            throw new InvalidOperationException($"Round '{roundState.Id}' is missing.");
        }

        // Be aware: at this point we are already in connection confirmation and all the coins got their first confirmation, so this is not exactly the starting time of the phase.
        var estimatedRemainingFromConnectionConfirmation = DateTimeOffset.UtcNow + roundState.CoinjoinState.Parameters.ConnectionConfirmationTimeout;

        CoinJoinClientProgress.SafeInvoke(this, new EnteringConnectionConfirmationPhase(newRoundState, estimatedRemainingFromConnectionConfirmation));

        return(result);
    }
Beispiel #2
0
    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);
    }
Beispiel #3
0
 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));
 }
Beispiel #4
0
    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);
    }
Beispiel #5
0
    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);
    }
Beispiel #6
0
    private async Task <ImmutableArray <(AliceClient AliceClient, PersonCircuit PersonCircuit)> > CreateRegisterAndConfirmCoinsAsync(IEnumerable <SmartCoin> smartCoins, RoundState roundState, CancellationToken cancellationToken)
    {
        int eventInvokedAlready = 0;

        async Task <(AliceClient?AliceClient, PersonCircuit?PersonCircuit)> RegisterInputAsync(SmartCoin coin, CancellationToken cancellationToken)
        {
            PersonCircuit?personCircuit = null;

            try
            {
                personCircuit = HttpClientFactory.NewHttpClientWithPersonCircuit(out Tor.Http.IHttpClient httpClient);

                // Alice client requests are inherently linkable to each other, so the circuit can be reused
                var arenaRequestHandler = new WabiSabiHttpApiClient(httpClient);

                var aliceArenaClient = new ArenaClient(
                    roundState.CreateAmountCredentialClient(SecureRandom),
                    roundState.CreateVsizeCredentialClient(SecureRandom),
                    arenaRequestHandler);

                var aliceClient = await AliceClient.CreateRegisterAndConfirmInputAsync(roundState, aliceArenaClient, coin, KeyChain, RoundStatusUpdater, cancellationToken).ConfigureAwait(false);

                // Right after the first real-cred confirmation happened we entered into critical phase.
                if (Interlocked.Exchange(ref eventInvokedAlready, 1) == 0)
                {
                    CoinJoinClientProgress.SafeInvoke(this, new EnteringCriticalPhase());
                }

                return(aliceClient, personCircuit);
            }
            catch (WabiSabiProtocolException)
            {
                personCircuit?.Dispose();
                return(null, null);
            }
            catch (Exception)
            {
                personCircuit?.Dispose();
                throw;
            }
        }

        // Gets the list of scheduled dates/time in the remaining available time frame when each alice has to be registered.
        var remainingTimeForRegistration = roundState.InputRegistrationEnd - DateTimeOffset.UtcNow;

        roundState.LogDebug($"Inputs({smartCoins.Count()}) registration started - it will end in: {remainingTimeForRegistration:hh\\:mm\\:ss}.");

        var scheduledDates = GetScheduledDates(smartCoins.Count(), roundState.InputRegistrationEnd);

        // Creates scheduled tasks (tasks that wait until the specified date/time and then perform the real registration)
        var aliceClients = smartCoins.Zip(
            scheduledDates,
            async(coin, date) =>
        {
            var delay = date - DateTimeOffset.UtcNow;
            if (delay > TimeSpan.Zero)
            {
                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
            }
            return(await RegisterInputAsync(coin, cancellationToken).ConfigureAwait(false));
        })
                           .ToImmutableArray();

        await Task.WhenAll(aliceClients).ConfigureAwait(false);

        return(aliceClients
               .Select(x => x.Result)
               .Where(r => r.AliceClient is not null && r.PersonCircuit is not null)
               .Select(r => (r.AliceClient !, r.PersonCircuit !))
               .ToImmutableArray());
    }
Beispiel #7
0
    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));
        }
    }