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); }
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); }
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); }
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); }
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); }
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); }
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); }
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); }
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)) });
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); }
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); }
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 }