public async Task SuccessWithAliceUpdateIntraRoundAsync()
    {
        WabiSabiConfig cfg   = new();
        var            round = WabiSabiFactory.CreateRound(cfg);

        using Key key = new();
        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);
        var coin           = WabiSabiFactory.CreateCoin(key);

        // Make sure an Alice have already been registered with the same input.
        var preAlice = WabiSabiFactory.CreateAlice(coin, WabiSabiFactory.CreateOwnershipProof(key), round);

        round.Alices.Add(preAlice);

        var rpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin);

        using Arena arena = await ArenaBuilder.From(cfg).With(rpc).CreateAndStartAsync(round);

        var arenaClient = WabiSabiFactory.CreateArenaClient(arena);
        var ex          = await Assert.ThrowsAsync <WabiSabiProtocolException>(async() => await arenaClient.RegisterInputAsync(round.Id, coin.Outpoint, ownershipProof, CancellationToken.None).ConfigureAwait(false));

        Assert.Equal(WabiSabiProtocolErrorCode.AliceAlreadyRegistered, ex.ErrorCode);

        await arena.StopAsync(CancellationToken.None);
    }
        public async Task InputRegistrationTimeoutCanBeModifiedRuntimeAsync()
        {
            WabiSabiConfig cfg = new() { StandardInputRegistrationTimeout = TimeSpan.FromHours(1) };

            using Key key = new();
            var coin = WabiSabiFactory.CreateCoin(key);

            using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, WabiSabiFactory.CreatePreconfiguredRpcClient(coin));

            await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));

            var round          = arena.Rounds.First();
            var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

            round.InputRegistrationTimeFrame = round.InputRegistrationTimeFrame with {
                Duration = TimeSpan.Zero
            };

            var arenaClient = WabiSabiFactory.CreateArenaClient(arena);
            var ex          = await Assert.ThrowsAsync <WabiSabiProtocolException>(
                async() => await arenaClient.RegisterInputAsync(round.Id, coin.Outpoint, ownershipProof, CancellationToken.None));

            Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
            Assert.Equal(Phase.InputRegistration, round.Phase);

            await arena.StopAsync(CancellationToken.None);
        }
示例#3
0
    public async Task InputRegistrationTimedoutAsync()
    {
        WabiSabiConfig cfg   = new() { StandardInputRegistrationTimeout = TimeSpan.Zero };
        var            round = WabiSabiFactory.CreateRound(cfg);

        using Key key = new();
        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

        var coin = WabiSabiFactory.CreateCoin(key);
        var rpc  = WabiSabiFactory.CreatePreconfiguredRpcClient(coin);

        using Arena arena = await ArenaBuilder.From(cfg).With(rpc).CreateAndStartAsync();

        await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));

        arena.Rounds.Add(round);

        var arenaClient = WabiSabiFactory.CreateArenaClient(arena);
        var ex          = await Assert.ThrowsAsync <WrongPhaseException>(
            async() => await arenaClient.RegisterInputAsync(round.Id, coin.Outpoint, ownershipProof, CancellationToken.None));

        Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
        Assert.Equal(Phase.InputRegistration, round.Phase);

        await arena.StopAsync(CancellationToken.None);
    }
    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 coinJoinIdsStore = new InMemoryCoinJoinIdStore();

        coinJoinIdsStore.Add(coin.Outpoint.Hash);
        using Arena arena = await ArenaBuilder.From(cfg).With(rpc).With(coinJoinIdsStore).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);
    }
示例#5
0
    public async Task InputImmatureAsync()
    {
        using Key key = new();
        WabiSabiConfig cfg            = new();
        var            round          = WabiSabiFactory.CreateRound(cfg);
        var            ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

        var rpc    = WabiSabiFactory.CreatePreconfiguredRpcClient();
        var rpcCfg = rpc.SetupSequence(rpc => rpc.GetTxOutAsync(It.IsAny <uint256>(), It.IsAny <int>(), It.IsAny <bool>(), It.IsAny <CancellationToken>()));

        foreach (var i in Enumerable.Range(1, 100))
        {
            rpcCfg = rpcCfg.ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse {
                Confirmations = i, IsCoinBase = true
            });
        }
        using Arena arena = await ArenaBuilder.From(cfg).With(rpc).CreateAndStartAsync(round);

        var arenaClient = WabiSabiFactory.CreateArenaClient(arena);

        var req = WabiSabiFactory.CreateInputRegistrationRequest(round: round);

        foreach (var i in Enumerable.Range(1, 100))
        {
            var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(
                async() => await arenaClient.RegisterInputAsync(round.Id, BitcoinFactory.CreateOutPoint(), ownershipProof, CancellationToken.None));

            Assert.Equal(WabiSabiProtocolErrorCode.InputImmature, ex.ErrorCode);
        }

        await arena.StopAsync(CancellationToken.None);
    }
示例#6
0
    public async Task InputRegistrationFullAsync()
    {
        WabiSabiConfig cfg   = new() { MaxInputCountByRound = 3 };
        var            round = WabiSabiFactory.CreateRound(cfg);

        using Key key = new();
        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

        using Arena arena = await ArenaBuilder.From(cfg).CreateAndStartAsync(round);

        await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21));

        round.Alices.Add(WabiSabiFactory.CreateAlice(round));
        round.Alices.Add(WabiSabiFactory.CreateAlice(round));
        round.Alices.Add(WabiSabiFactory.CreateAlice(round));

        var arenaClient = WabiSabiFactory.CreateArenaClient(arena);
        var ex          = await Assert.ThrowsAsync <WrongPhaseException>(
            async() => await arenaClient.RegisterInputAsync(round.Id, BitcoinFactory.CreateOutPoint(), ownershipProof, CancellationToken.None));

        Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode);
        Assert.Equal(Phase.InputRegistration, round.Phase);

        await arena.StopAsync(CancellationToken.None);
    }
示例#7
0
    public async Task InputUnconfirmedAsync()
    {
        using Key key = new();
        WabiSabiConfig cfg            = new();
        var            round          = WabiSabiFactory.CreateRound(cfg);
        var            ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

        var mockRpc = new Mock <IRPCClient>();

        mockRpc.Setup(rpc => rpc.GetTxOutAsync(It.IsAny <uint256>(), It.IsAny <int>(), It.IsAny <bool>(), It.IsAny <CancellationToken>()))
        .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse {
            Confirmations = 0
        });

        using Arena arena = await ArenaBuilder.From(cfg).With(mockRpc).CreateAndStartAsync(round);

        var arenaClient = WabiSabiFactory.CreateArenaClient(arena);

        var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(
            async() => await arenaClient.RegisterInputAsync(round.Id, BitcoinFactory.CreateOutPoint(), ownershipProof, CancellationToken.None));

        Assert.Equal(WabiSabiProtocolErrorCode.InputUnconfirmed, ex.ErrorCode);

        await arena.StopAsync(CancellationToken.None);
    }
示例#8
0
    public async Task InputCanBeNotedAsync()
    {
        using Key key = new();
        var outpoint = BitcoinFactory.CreateOutPoint();

        WabiSabiConfig cfg   = new();
        var            round = WabiSabiFactory.CreateRound(cfg);

        Prison prison = new();

        using Arena arena = await ArenaBuilder.From(cfg, prison).CreateAndStartAsync(round);

        prison.Punish(outpoint, Punishment.Noted, uint256.One);

        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key);

        var arenaClient = WabiSabiFactory.CreateArenaClient(arena);

        var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(
            async() => await arenaClient.RegisterInputAsync(round.Id, outpoint, ownershipProof, CancellationToken.None));

        Assert.NotEqual(WabiSabiProtocolErrorCode.InputBanned, ex.ErrorCode);

        await arena.StopAsync(CancellationToken.None);
    }
示例#9
0
    public async Task RegisterBannedCoinAsync()
    {
        using CancellationTokenSource timeoutCts = new(TimeSpan.FromMinutes(2));

        var bannedOutPoint = BitcoinFactory.CreateOutPoint();

        var httpClient = _apiApplicationFactory.WithWebHostBuilder(builder =>
                                                                   builder.ConfigureServices(services =>
        {
            var inmate = new Inmate(bannedOutPoint, Punishment.LongBanned, DateTimeOffset.UtcNow, uint256.One);
            services.AddScoped <Prison>(_ => new Prison(new[] { inmate }));
        })).CreateClient();

        var apiClient = await _apiApplicationFactory.CreateArenaClientAsync(httpClient);

        var rounds = (await apiClient.GetStatusAsync(RoundStateRequest.Empty, timeoutCts.Token)).RoundStates;
        var round  = rounds.First(x => x.CoinjoinState is ConstructionState);

        // If an output is not in the utxo dataset then it is not unspent, this
        // means that the output is spent or simply doesn't even exist.
        using var signingKey = new Key();
        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(signingKey, round.Id);

        var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(async() =>
                                                                      await apiClient.RegisterInputAsync(round.Id, bannedOutPoint, ownershipProof, timeoutCts.Token));

        Assert.Equal(WabiSabiProtocolErrorCode.InputLongBanned, ex.ErrorCode);
        var inputBannedData = Assert.IsType <InputBannedExceptionData>(ex.ExceptionData);

        Assert.True(inputBannedData.BannedUntil > DateTimeOffset.UtcNow);
    }
示例#10
0
    public async Task RoundNotFoundAsync()
    {
        using Key key     = new();
        using Arena arena = await ArenaBuilder.Default.CreateAndStartAsync();

        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key);

        var arenaClient = WabiSabiFactory.CreateArenaClient(arena);
        var ex          = await Assert.ThrowsAsync <WabiSabiProtocolException>(
            async() => await arenaClient.RegisterInputAsync(uint256.Zero, BitcoinFactory.CreateOutPoint(), ownershipProof, CancellationToken.None));

        Assert.Equal(WabiSabiProtocolErrorCode.RoundNotFound, ex.ErrorCode);

        await arena.StopAsync(CancellationToken.None);
    }
示例#11
0
    public async Task ScriptNotAllowedAsync()
    {
        WabiSabiConfig cfg   = new();
        var            round = WabiSabiFactory.CreateRound(cfg);

        using Key key = new();
        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

        var mockRpc = new Mock <IRPCClient>();

        mockRpc.Setup(rpc => rpc.GetTxOutAsync(It.IsAny <uint256>(), It.IsAny <int>(), It.IsAny <bool>(), It.IsAny <CancellationToken>()))
        .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse
        {
            Confirmations = 1,
            TxOut         = new(Money.Coins(1), key.PubKey.ScriptPubKey.Hash.GetAddress(Network.Main))
        });
示例#12
0
        public async Task SuccessAsync()
        {
            WabiSabiConfig cfg   = new();
            var            round = WabiSabiFactory.CreateRound(cfg);

            using Key key = new();
            var coin = WabiSabiFactory.CreateCoin(key);

            using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, WabiSabiFactory.CreatePreconfiguredRpcClient(coin), 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);

            await arena.StopAsync(CancellationToken.None);
        }
示例#13
0
    public async Task RegisterSpentOrInNonExistentCoinAsync()
    {
        var httpClient = _apiApplicationFactory.CreateClient();

        var apiClient = await _apiApplicationFactory.CreateArenaClientAsync(httpClient);

        var rounds = (await apiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None)).RoundStates;
        var round  = rounds.First(x => x.CoinjoinState is ConstructionState);

        // If an output is not in the utxo dataset then it is not unspent, this
        // means that the output is spent or simply doesn't even exist.
        var nonExistingOutPoint = new OutPoint();

        using var signingKey = new Key();
        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(signingKey, round.Id);

        var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(async() =>
                                                                      await apiClient.RegisterInputAsync(round.Id, nonExistingOutPoint, ownershipProof, CancellationToken.None));

        Assert.Equal(WabiSabiProtocolErrorCode.InputSpent, ex.ErrorCode);
    }
示例#14
0
    public async Task FullCoinjoinAsyncTestAsync()
    {
        var config = new WabiSabiConfig {
            MaxInputCountByRound = 1
        };
        var round = WabiSabiFactory.CreateRound(WabiSabiFactory.CreateRoundParameters(config));

        using var key = new Key();
        var outpoint = BitcoinFactory.CreateOutPoint();
        var mockRpc  = new Mock <IRPCClient>();

        mockRpc.Setup(rpc => rpc.GetTxOutAsync(outpoint.Hash, (int)outpoint.N, true, It.IsAny <CancellationToken>()))
        .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse
        {
            IsCoinBase    = false,
            Confirmations = 200,
            TxOut         = new TxOut(Money.Coins(1m), key.PubKey.WitHash.GetAddress(Network.Main)),
        });
        mockRpc.Setup(rpc => rpc.EstimateSmartFeeAsync(It.IsAny <int>(), It.IsAny <EstimateSmartFeeMode>(), It.IsAny <CancellationToken>()))
        .ReturnsAsync(new EstimateSmartFeeResponse
        {
            Blocks  = 1000,
            FeeRate = new FeeRate(10m)
        });
        mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny <CancellationToken>()))
        .ReturnsAsync(new MemPoolInfo
        {
            MinRelayTxFee = 1
        });
        mockRpc.Setup(rpc => rpc.PrepareBatch()).Returns(mockRpc.Object);
        mockRpc.Setup(rpc => rpc.SendBatchAsync(It.IsAny <CancellationToken>())).Returns(Task.CompletedTask);
        mockRpc.Setup(rpc => rpc.GetRawTransactionAsync(It.IsAny <uint256>(), It.IsAny <bool>(), It.IsAny <CancellationToken>()))
        .ReturnsAsync(BitcoinFactory.CreateTransaction());

        using Arena arena = await ArenaBuilder.From(config).With(mockRpc).CreateAndStartAsync(round);

        await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1));

        using var memoryCache = new MemoryCache(new MemoryCacheOptions());
        var idempotencyRequestCache = new IdempotencyRequestCache(memoryCache);

        using CoinJoinFeeRateStatStore coinJoinFeeRateStatStore = new(config, arena.Rpc);
        var wabiSabiApi = new WabiSabiController(idempotencyRequestCache, arena, coinJoinFeeRateStatStore);

        var insecureRandom   = new InsecureRandom();
        var roundState       = RoundState.FromRound(round);
        var aliceArenaClient = new ArenaClient(
            roundState.CreateAmountCredentialClient(insecureRandom),
            roundState.CreateVsizeCredentialClient(insecureRandom),
            wabiSabiApi);
        var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);

        var(inputRegistrationResponse, _) = await aliceArenaClient.RegisterInputAsync(round.Id, outpoint, ownershipProof, CancellationToken.None);

        var aliceId = inputRegistrationResponse.Value;

        var inputVsize       = Constants.P2wpkhInputVirtualSize;
        var amountsToRequest = new[]
        {
            Money.Coins(.75m) - round.Parameters.MiningFeeRate.GetFee(inputVsize) - round.Parameters.CoordinationFeeRate.GetFee(Money.Coins(1m)),
            Money.Coins(.25m),
        }.Select(x => x.Satoshi).ToArray();

        using var destinationKey1 = new Key();
        using var destinationKey2 = new Key();
        var p2wpkhScriptSize = (long)destinationKey1.PubKey.WitHash.ScriptPubKey.EstimateOutputVsize();

        var vsizesToRequest = new[] { round.Parameters.MaxVsizeAllocationPerAlice - (inputVsize + 2 * p2wpkhScriptSize), 2 * p2wpkhScriptSize };

        // Phase: Input Registration
        Assert.Equal(Phase.InputRegistration, round.Phase);

        var connectionConfirmationResponse1 = await aliceArenaClient.ConfirmConnectionAsync(
            round.Id,
            aliceId,
            amountsToRequest,
            vsizesToRequest,
            inputRegistrationResponse.IssuedAmountCredentials,
            inputRegistrationResponse.IssuedVsizeCredentials,
            CancellationToken.None);

        await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1));

        Assert.Equal(Phase.ConnectionConfirmation, round.Phase);

        // Phase: Connection Confirmation
        var connectionConfirmationResponse2 = await aliceArenaClient.ConfirmConnectionAsync(
            round.Id,
            aliceId,
            amountsToRequest,
            vsizesToRequest,
            connectionConfirmationResponse1.IssuedAmountCredentials,
            connectionConfirmationResponse1.IssuedVsizeCredentials,
            CancellationToken.None);

        await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(1));

        // Phase: Output Registration
        Assert.Equal(Phase.OutputRegistration, round.Phase);

        var bobArenaClient = new ArenaClient(
            roundState.CreateAmountCredentialClient(insecureRandom),
            roundState.CreateVsizeCredentialClient(insecureRandom),
            wabiSabiApi);

        var reissuanceResponse = await bobArenaClient.ReissueCredentialAsync(
            round.Id,
            amountsToRequest,
            Enumerable.Repeat(p2wpkhScriptSize, 2),
            connectionConfirmationResponse2.IssuedAmountCredentials.Take(ProtocolConstants.CredentialNumber),
            connectionConfirmationResponse2.IssuedVsizeCredentials.Skip(1).Take(ProtocolConstants.CredentialNumber),             // first amount is the leftover value
            CancellationToken.None);

        Credential amountCred1     = reissuanceResponse.IssuedAmountCredentials.ElementAt(0);
        Credential amountCred2     = reissuanceResponse.IssuedAmountCredentials.ElementAt(1);
        Credential zeroAmountCred1 = reissuanceResponse.IssuedAmountCredentials.ElementAt(2);
        Credential zeroAmountCred2 = reissuanceResponse.IssuedAmountCredentials.ElementAt(3);

        Credential vsizeCred1     = reissuanceResponse.IssuedVsizeCredentials.ElementAt(0);
        Credential vsizeCred2     = reissuanceResponse.IssuedVsizeCredentials.ElementAt(1);
        Credential zeroVsizeCred1 = reissuanceResponse.IssuedVsizeCredentials.ElementAt(2);
        Credential zeroVsizeCred2 = reissuanceResponse.IssuedVsizeCredentials.ElementAt(3);

        await bobArenaClient.RegisterOutputAsync(
            round.Id,
            destinationKey1.PubKey.WitHash.ScriptPubKey,
            new[] { amountCred1, zeroAmountCred1 },
            new[] { vsizeCred1, zeroVsizeCred1 },
            CancellationToken.None);

        await bobArenaClient.RegisterOutputAsync(
            round.Id,
            destinationKey2.PubKey.WitHash.ScriptPubKey,
            new[] { amountCred2, zeroAmountCred2 },
            new[] { vsizeCred2, zeroVsizeCred2 },
            CancellationToken.None);

        await aliceArenaClient.ReadyToSignAsync(round.Id, aliceId, CancellationToken.None);

        await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1));

        Assert.Equal(Phase.TransactionSigning, round.Phase);

        var tx = round.Assert <SigningState>().CreateTransaction();

        Assert.Single(tx.Inputs);
        Assert.Equal(2 + 1, tx.Outputs.Count);         // +1 because it pays coordination fees
    }