Exemple #1
0
    public async Task SuccessFromPreviousCoinJoinAsync()
    {
        WabiSabiConfig cfg   = new();
        var            round = WabiSabiFactory.CreateRound(cfg);

        using Key key = new();
        var coin            = WabiSabiFactory.CreateCoin(key);
        var rpc             = WabiSabiFactory.CreatePreconfiguredRpcClient(coin);
        var coinJoinIdStore = new CoinJoinIdStore();

        coinJoinIdStore.TryAdd(coin.Outpoint.Hash);
        using Arena arena = await ArenaBuilder.From(cfg).With(rpc).With(coinJoinIdStore).CreateAndStartAsync(round);

        var minAliceDeadline = DateTimeOffset.UtcNow + cfg.ConnectionConfirmationTimeout * 0.9;
        var arenaClient      = WabiSabiFactory.CreateArenaClient(arena);
        var ownershipProof   = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

        var(resp, _) = await arenaClient.RegisterInputAsync(round.Id, coin.Outpoint, ownershipProof, CancellationToken.None);

        AssertSingleAliceSuccessfullyRegistered(round, minAliceDeadline, resp);

        var myAlice = Assert.Single(round.Alices);

        Assert.True(myAlice.IsPayingZeroCoordinationFee);

        await arena.StopAsync(CancellationToken.None);
    }
    public void CanAdd()
    {
        var cjIdStore = new CoinJoinIdStore();

        cjIdStore.TryAdd(uint256.One);

        Assert.True(cjIdStore.Contains(uint256.One));

        cjIdStore.TryAdd(uint256.One);
        cjIdStore.TryAdd(uint256.One);

        Assert.Single(cjIdStore.GetCoinJoinIds);
    }
    public void CanTolerateError()
    {
        var listOfCoinjoinHashes = new List <string>
        {
            "9690826aab03c7b9ca15af2d79083445df1ac94e79858acc146efa9a52c73b5b",
            "dummy",
            "90f1e3893a890ae314fba50c3dc870b0b5e5aab6e14f9e0fe9e56c95f20a2b36",
            "1fa685dbf8273369762a4f88ad1ce7f3fd14907130b878fbbb96e4140bf2bc96"
        };

        IEnumerable <uint256> ids = CoinJoinIdStore.GetValidCoinjoinIds(listOfCoinjoinHashes, out var validCoinjoinIds, out bool wasError);

        Assert.Equal(ids.Count(), validCoinjoinIds.Count);
        Assert.Equal(ids.Count(), listOfCoinjoinHashes.Count - 1);
        Assert.True(wasError);
    }
    public void CanValidate()
    {
        var listOfCoinjoinHashes = new List <string>
        {
            "9690826aab03c7b9ca15af2d79083445df1ac94e79858acc146efa9a52c73b5b",
            "a79e7544d32f9c5e0c3a6ed9bcbb29723125f38461bb7a735823eddc7dac7ad2",
            "90f1e3893a890ae314fba50c3dc870b0b5e5aab6e14f9e0fe9e56c95f20a2b36",
            "1fa685dbf8273369762a4f88ad1ce7f3fd14907130b878fbbb96e4140bf2bc96"
        };

        IEnumerable <uint256> ids = CoinJoinIdStore.GetValidCoinjoinIds(listOfCoinjoinHashes, out var validCoinjoinIds, out bool wasError);

        Assert.Equal(listOfCoinjoinHashes.Count, ids.Count());
        Assert.Equal(listOfCoinjoinHashes, validCoinjoinIds);
        Assert.False(wasError);
    }
Exemple #5
0
    private void Arena_CoinJoinBroadcast(object?sender, Transaction transaction)
    {
        LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow;

        CoinJoinIdStore.TryAdd(transaction.GetHash());

        var coinJoinScriptStoreFilePath = Parameters.CoinJoinScriptStoreFilePath;

        try
        {
            File.AppendAllLines(coinJoinScriptStoreFilePath, transaction.Outputs.Select(x => x.ScriptPubKey.ToHex()));
        }
        catch (Exception ex)
        {
            Logger.LogError($"Could not write file {coinJoinScriptStoreFilePath}.", ex);
        }
    }
Exemple #6
0
    public async Task InitializeAsync(Config config, CoordinatorRoundConfig roundConfig, IRPCClient rpc, CancellationToken cancel)
    {
        Config      = Guard.NotNull(nameof(config), config);
        RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig);
        RpcClient   = Guard.NotNull(nameof(rpc), rpc);

        // Make sure RPC works.
        await AssertRpcNodeFullyInitializedAsync(cancel);

        // Make sure P2P works.
        await InitializeP2pAsync(config.Network, config.GetBitcoinP2pEndPoint(), cancel);

        var p2pNode = Guard.NotNull(nameof(P2pNode), P2pNode);

        HostedServices.Register <MempoolMirror>(() => new MempoolMirror(TimeSpan.FromSeconds(21), RpcClient, p2pNode), "Full Node Mempool Mirror");

        // Initialize index building
        var indexBuilderServiceDir = Path.Combine(DataDir, "IndexBuilderService");
        var indexFilePath          = Path.Combine(indexBuilderServiceDir, $"Index{RpcClient.Network}.dat");
        var blockNotifier          = HostedServices.Get <BlockNotifier>();

        CoordinatorParameters coordinatorParameters = new(DataDir);

        Coordinator = new(RpcClient.Network, blockNotifier, Path.Combine(DataDir, "CcjCoordinator"), RpcClient, roundConfig);
        Coordinator.CoinJoinBroadcasted += Coordinator_CoinJoinBroadcasted;

        var coordinator = Guard.NotNull(nameof(Coordinator), Coordinator);

        if (!string.IsNullOrWhiteSpace(roundConfig.FilePath))
        {
            HostedServices.Register <ConfigWatcher>(() =>
                                                    new ConfigWatcher(
                                                        TimeSpan.FromSeconds(10), // Every 10 seconds check the config
                                                        RoundConfig,
                                                        () =>
            {
                try
                {
                    coordinator.RoundConfig.UpdateOrDefault(RoundConfig, toFile: false);

                    coordinator.AbortAllRoundsInInputRegistration($"{nameof(RoundConfig)} has changed.");
                }
                catch (Exception ex)
                {
                    Logger.LogDebug(ex);
                }
            }),
                                                    "Config Watcher");
        }

        CoinJoinIdStore = CoinJoinIdStore.Create(Coordinator.CoinJoinsFilePath, coordinatorParameters.CoinJoinIdStoreFilePath);
        var coinJoinScriptStore = CoinJoinScriptStore.LoadFromFile(coordinatorParameters.CoinJoinScriptStoreFilePath);

        WabiSabiCoordinator = new WabiSabiCoordinator(coordinatorParameters, RpcClient, CoinJoinIdStore, coinJoinScriptStore);
        HostedServices.Register <WabiSabiCoordinator>(() => WabiSabiCoordinator, "WabiSabi Coordinator");
        HostedServices.Register <RoundBootstrapper>(() => new RoundBootstrapper(TimeSpan.FromMilliseconds(100), Coordinator), "Round Bootstrapper");

        await HostedServices.StartAllAsync(cancel);

        IndexBuilderService = new(RpcClient, blockNotifier, indexFilePath);
        IndexBuilderService.Synchronize();
        Logger.LogInfo($"{nameof(IndexBuilderService)} is successfully initialized and started synchronization.");
    }
Exemple #7
0
    private async Task <InputRegistrationResponse> RegisterInputCoreAsync(InputRegistrationRequest request, CancellationToken cancellationToken)
    {
        var coin = await OutpointToCoinAsync(request, cancellationToken).ConfigureAwait(false);

        using (await AsyncLock.LockAsync(cancellationToken).ConfigureAwait(false))
        {
            var round = GetRound(request.RoundId);

            var registeredCoins = Rounds.Where(x => !(x.Phase == Phase.Ended && !x.WasTransactionBroadcast))
                                  .SelectMany(r => r.Alices.Select(a => a.Coin));

            if (registeredCoins.Any(x => x.Outpoint == coin.Outpoint))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceAlreadyRegistered);
            }

            if (round.IsInputRegistrationEnded(Config.MaxInputCountByRound))
            {
                throw new WrongPhaseException(round, Phase.InputRegistration);
            }

            if (round is BlameRound blameRound && !blameRound.BlameWhitelist.Contains(coin.Outpoint))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputNotWhitelisted);
            }

            // Compute but don't commit updated coinjoin to round state, it will
            // be re-calculated on input confirmation. This is computed in here
            // for validation purposes.
            _ = round.Assert <ConstructionState>().AddInput(coin);

            var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData("CoinJoinCoordinatorIdentifier", round.Id);
            if (!OwnershipProof.VerifyCoinJoinInputProof(request.OwnershipProof, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData))
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongOwnershipProof);
            }

            // Generate a new GUID with the secure random source, to be sure
            // that it is not guessable (Guid.NewGuid() documentation does
            // not say anything about GUID version or randomness source,
            // only that the probability of duplicates is very low).
            var id = new Guid(SecureRandom.Instance.GetBytes(16));

            var isPayingZeroCoordinationFee = CoinJoinIdStore.Contains(coin.Outpoint.Hash);

            if (!isPayingZeroCoordinationFee)
            {
                // If the coin comes from a tx that all of the tx inputs are coming from a CJ (1 hop - no pay).
                Transaction tx = await Rpc.GetRawTransactionAsync(coin.Outpoint.Hash, true, cancellationToken).ConfigureAwait(false);

                if (tx.Inputs.All(input => CoinJoinIdStore.Contains(input.PrevOut.Hash)))
                {
                    isPayingZeroCoordinationFee = true;
                }
            }

            var alice = new Alice(coin, request.OwnershipProof, round, id, isPayingZeroCoordinationFee);

            if (alice.CalculateRemainingAmountCredentials(round.Parameters.MiningFeeRate, round.Parameters.CoordinationFeeRate) <= Money.Zero)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.UneconomicalInput);
            }

            if (alice.TotalInputAmount < round.Parameters.MinAmountCredentialValue)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds);
            }
            if (alice.TotalInputAmount > round.Parameters.MaxAmountCredentialValue)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds);
            }

            if (alice.TotalInputVsize > round.Parameters.MaxVsizeAllocationPerAlice)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchVsize);
            }

            var amountCredentialTask = round.AmountCredentialIssuer.HandleRequestAsync(request.ZeroAmountCredentialRequests, cancellationToken);
            var vsizeCredentialTask  = round.VsizeCredentialIssuer.HandleRequestAsync(request.ZeroVsizeCredentialRequests, cancellationToken);

            if (round.RemainingInputVsizeAllocation < round.Parameters.MaxVsizeAllocationPerAlice)
            {
                throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.VsizeQuotaExceeded);
            }

            var commitAmountCredentialResponse = await amountCredentialTask.ConfigureAwait(false);

            var commitVsizeCredentialResponse = await vsizeCredentialTask.ConfigureAwait(false);

            alice.SetDeadlineRelativeTo(round.ConnectionConfirmationTimeFrame.Duration);
            round.Alices.Add(alice);

            return(new(alice.Id,
                       commitAmountCredentialResponse,
                       commitVsizeCredentialResponse,
                       alice.IsPayingZeroCoordinationFee));
        }
    }