public async Task InputWhitelistedButBannedAsync() { using Key key = new(); var alice = WabiSabiFactory.CreateAlice(key); var bannedCoin = alice.Coin.Outpoint; WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); round.Alices.Add(alice); Round blameRound = WabiSabiFactory.CreateBlameRound(round, cfg); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(alice.Coin); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc, round, blameRound); arena.Prison.Punish(bannedCoin, Punishment.Banned, uint256.Zero); await using ArenaRequestHandler handler = new(cfg, arena.Prison, arena, mockRpc.Object); var req = WabiSabiFactory.CreateInputRegistrationRequest(key: key, round: blameRound, prevout: bannedCoin); var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(async() => await handler.RegisterInputAsync(req, CancellationToken.None)); Assert.Equal(WabiSabiProtocolErrorCode.InputBanned, ex.ErrorCode); await arena.StopAsync(CancellationToken.None); }
public async Task DoesntSwitchImmaturelyAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; var(key1, coin1, key2, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc); var(round, arenaClient, alices) = await CreateRoundWithTwoConfirmedConnectionsAsync(arena, key1, coin1, key2, coin2); var(amountCredentials1, vsizeCredentials1) = (alices[0].IssuedAmountCredentials, alices[0].IssuedVsizeCredentials); var(amountCredentials2, vsizeCredentials2) = (alices[1].IssuedAmountCredentials, alices[1].IssuedVsizeCredentials); // Register outputs. var bobClient = new BobClient(round.Id, arenaClient); using var destKey = new Key(); await bobClient.RegisterOutputAsync( destKey.PubKey.WitHash.ScriptPubKey, amountCredentials1.Take(ProtocolConstants.CredentialNumber), vsizeCredentials1.Take(ProtocolConstants.CredentialNumber), CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); await arena.StopAsync(CancellationToken.None); }
public async Task TimeoutSufficientPeersAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 1, TransactionSigningTimeout = TimeSpan.Zero, OutputRegistrationTimeout = TimeSpan.Zero }; var(keyChain, coin1, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); mockRpc.Setup(rpc => rpc.SendRawTransactionAsync(It.IsAny <Transaction>(), It.IsAny <CancellationToken>())) .ThrowsAsync(new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null)); Prison prison = new(); using Arena arena = await ArenaBuilder.From(cfg, mockRpc, prison).CreateAndStartAsync(); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, keyChain, coin1, coin2); // Make sure not all alices signed. var alice3 = WabiSabiFactory.CreateAlice(round); alice3.ConfirmedConnection = true; round.Alices.Add(alice3); round.CoinjoinState = round.Assert <ConstructionState>().AddInput(alice3.Coin); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await aliceClient2.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.Rounds.Where(x => x.Phase != Phase.Ended)); Assert.Single(arena.Rounds.Where(x => x is BlameRound)); var badOutpoint = alice3.Coin.Outpoint; Assert.Contains(badOutpoint, prison.GetInmates().Select(x => x.Utxo)); var onlyRound = arena.Rounds.Single(x => x is BlameRound); var blameRound = Assert.IsType <BlameRound>(onlyRound); Assert.NotNull(blameRound.BlameOf); Assert.Equal(round.Id, blameRound.BlameOf.Id); var whitelist = blameRound.BlameWhitelist; Assert.Contains(aliceClient1.SmartCoin.OutPoint, whitelist); Assert.Contains(aliceClient2.SmartCoin.OutPoint, whitelist); Assert.DoesNotContain(badOutpoint, whitelist); 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 WabiSabiFactory.CreateAndStartArenaAsync(cfg, WabiSabiFactory.CreatePreconfiguredRpcClient(), 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 <WabiSabiProtocolException>( 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 WrongPhaseAsync() { WabiSabiConfig cfg = new(); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)).ConfigureAwait(false); var round = arena.Rounds.First(); using Key key = new(); var req = WabiSabiFactory.CreateInputRegistrationRequest(round, key: key); await using ArenaRequestHandler handler = new(cfg, new(), arena, WabiSabiFactory.CreatePreconfiguredRpcClient().Object); foreach (Phase phase in Enum.GetValues(typeof(Phase))) { if (phase != Phase.InputRegistration) { round.SetPhase(phase); await RegisterAndAssertWrongPhaseAsync(req, handler); } } 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 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 DetectSpentTxoBeforeSteppingIntoConnectionConfirmationAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 3 }; var round = WabiSabiFactory.CreateRound(cfg); var offendingAlice = WabiSabiFactory.CreateAlice(round); // this Alice spent the coin after registration var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(); mockRpc.Setup(rpc => rpc.GetTxOutAsync(offendingAlice.Coin.Outpoint.Hash, (int)offendingAlice.Coin.Outpoint.N, true, It.IsAny <CancellationToken>())) .ReturnsAsync((GetTxOutResponse?)null); using Arena arena = await ArenaBuilder.From(cfg).With(mockRpc).CreateAndStartAsync(round); round.Alices.Add(WabiSabiFactory.CreateAlice(round)); round.Alices.Add(offendingAlice); round.Alices.Add(WabiSabiFactory.CreateAlice(round)); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.InputRegistration, round.Phase); Assert.Equal(2, round.Alices.Count); // the offending alice was removed round.Alices.Add(WabiSabiFactory.CreateAlice(round)); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.ConnectionConfirmation, round.Phase); Assert.Equal(3, round.Alices.Count); await arena.StopAsync(CancellationToken.None); }
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 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 AliceTimesoutAsync() { // Alice times out when its deadline is reached. WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); using Key key = new(); var coin = WabiSabiFactory.CreateCoin(key); var rpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, rpc, round); var arenaClient = WabiSabiFactory.CreateArenaClient(arena); // Register Alices. var minAliceDeadline = DateTimeOffset.UtcNow + cfg.ConnectionConfirmationTimeout * 0.9; var aliceClient = new AliceClient(round.Id, arenaClient, coin, round.FeeRate, key.GetBitcoinSecret(round.Network)); await aliceClient.RegisterInputAsync(CancellationToken.None).ConfigureAwait(false); var alice = Assert.Single(round.Alices); alice.Deadline = DateTimeOffset.UtcNow - TimeSpan.FromMilliseconds(1); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Empty(round.Alices); await arena.StopAsync(CancellationToken.None); }
public async Task EveryoneSignedAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; using Key key1 = new(); using Key key2 = new(); var coin1 = WabiSabiFactory.CreateCoin(key1); var coin2 = WabiSabiFactory.CreateCoin(key2); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1, coin2); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, key1, coin1, key2, coin2).ConfigureAwait(false); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, CancellationToken.None); await aliceClient2.SignTransactionAsync(signedCoinJoin, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionBroadcasting, round.Phase); 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 DiffTooSmallToBlameAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5, OutputRegistrationTimeout = TimeSpan.Zero, CoordinationFeeRate = CoordinationFeeRate.Zero }; var(keyChain, coin1, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); using Arena arena = await ArenaBuilder.From(cfg).With(mockRpc).CreateAndStartAsync(); var(round, arenaClient, alices) = await CreateRoundWithTwoConfirmedConnectionsAsync(arena, keyChain, coin1, coin2); var(amountCredentials1, vsizeCredentials1) = (alices[0].IssuedAmountCredentials, alices[0].IssuedVsizeCredentials); var(amountCredentials2, vsizeCredentials2) = (alices[1].IssuedAmountCredentials, alices[1].IssuedVsizeCredentials); // Register outputs. var bobClient = new BobClient(round.Id, arenaClient); using var destKey1 = new Key(); using var destKey2 = new Key(); await bobClient.RegisterOutputAsync( destKey1.PubKey.WitHash.ScriptPubKey, amountCredentials1.Take(ProtocolConstants.CredentialNumber), vsizeCredentials1.Take(ProtocolConstants.CredentialNumber), CancellationToken.None); await bobClient.RegisterOutputAsync( destKey2.PubKey.WitHash.ScriptPubKey, amountCredentials2.Take(ProtocolConstants.CredentialNumber), vsizeCredentials2.Take(ProtocolConstants.CredentialNumber), CancellationToken.None); // Add another input. The input must be able to pay for itself, but // the remaining amount after deducting the fees needs to be less // than the minimum. var txParams = round.Assert <ConstructionState>().Parameters; var extraAlice = WabiSabiFactory.CreateAlice(txParams.FeeRate.GetFee(Constants.P2wpkhInputVirtualSize) + txParams.AllowedOutputAmounts.Min - new Money(1L), round); round.Alices.Add(extraAlice); round.CoinjoinState = round.Assert <ConstructionState>().AddInput(extraAlice.Coin); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var tx = round.Assert <SigningState>().CreateTransaction(); Assert.Equal(3, tx.Inputs.Count); Assert.Equal(2, tx.Outputs.Count); Assert.DoesNotContain(round.CoordinatorScript, tx.Outputs.Select(x => x.ScriptPubKey)); await arena.StopAsync(CancellationToken.None); }
public async Task AllBobsRegisteredAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; using Key key1 = new(); using Key key2 = new(); var coin1 = WabiSabiFactory.CreateCoin(key1); var coin2 = WabiSabiFactory.CreateCoin(key2); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1, coin2); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc); var(round, arenaClient, alices) = await CreateRoundWithTwoConfirmedConnectionsAsync(arena, key1, coin1, key2, coin2); var(amountCredentials1, vsizeCredentials1) = (alices[0].IssuedAmountCredentials, alices[0].IssuedVsizeCredentials); var(amountCredentials2, vsizeCredentials2) = (alices[1].IssuedAmountCredentials, alices[1].IssuedVsizeCredentials); // Register outputs. var bobClient = new BobClient(round.Id, arenaClient); using var destKey1 = new Key(); await bobClient.RegisterOutputAsync( coin1.Amount - round.FeeRate.GetFee(coin1.ScriptPubKey.EstimateInputVsize()), destKey1.PubKey.WitHash.ScriptPubKey, amountCredentials1.Take(ProtocolConstants.CredentialNumber), vsizeCredentials1.Take(ProtocolConstants.CredentialNumber), CancellationToken.None); using var destKey2 = new Key(); await bobClient.RegisterOutputAsync( coin2.Amount - round.FeeRate.GetFee(coin2.ScriptPubKey.EstimateInputVsize()), destKey2.PubKey.WitHash.ScriptPubKey, amountCredentials2.Take(ProtocolConstants.CredentialNumber), vsizeCredentials2.Take(ProtocolConstants.CredentialNumber), CancellationToken.None); foreach (var alice in alices) { await alice.ReadyToSignAsync(CancellationToken.None); } await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var tx = round.Assert <SigningState>().CreateTransaction(); Assert.Equal(2, tx.Inputs.Count); Assert.Equal(2, tx.Outputs.Count); await arena.StopAsync(CancellationToken.None); }
public async Task AliceTimesoutAsync() { // Alice times out when its deadline is reached. WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); var km = ServiceFactory.CreateKeyManager(""); var key = BitcoinFactory.CreateHdPubKey(km); var smartCoin = BitcoinFactory.CreateSmartCoin(key, 10m); var rpc = WabiSabiFactory.CreatePreconfiguredRpcClient(smartCoin.Coin); using Arena arena = await ArenaBuilder.From(cfg).With(rpc).CreateAndStartAsync(round); var arenaClient = WabiSabiFactory.CreateArenaClient(arena); using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), arena); await roundStateUpdater.StartAsync(CancellationToken.None); // Register Alices. var keyChain = new KeyChain(km, new Kitchen("")); using CancellationTokenSource cancellationTokenSource = new(); var task = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, smartCoin, keyChain, roundStateUpdater, cancellationTokenSource.Token); while (round.Alices.Count == 0) { await Task.Delay(10); } var alice = Assert.Single(round.Alices); alice.Deadline = DateTimeOffset.UtcNow - TimeSpan.FromMilliseconds(1); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Empty(round.Alices); cancellationTokenSource.Cancel(); try { await task; throw new InvalidOperationException("The operation should throw!"); } catch (Exception exc) { Assert.True(exc is OperationCanceledException or WabiSabiProtocolException); } await roundStateUpdater.StopAsync(CancellationToken.None); await arena.StopAsync(CancellationToken.None); }
public async Task AlicesSpentAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; using Key key1 = new(); using Key key2 = new(); var coin1 = WabiSabiFactory.CreateCoin(key1); var coin2 = WabiSabiFactory.CreateCoin(key2); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1, coin2); mockRpc.Setup(rpc => rpc.SendRawTransactionAsync(It.IsAny <Transaction>())) .ThrowsAsync(new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null)); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, key1, coin1, key2, coin2); await aliceClient1.ReadyToSignAsync(CancellationToken.None); await aliceClient2.ReadyToSignAsync(CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, CancellationToken.None); await aliceClient2.SignTransactionAsync(signedCoinJoin, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.ActiveRounds); Assert.Equal(Phase.Ended, round.Phase); Assert.False(round.WasTransactionBroadcast); // There should be no inmate, because we aren't punishing spenders with banning // as there's no reason to ban already spent UTXOs, // the cost of spending the UTXO is the punishment instead. Assert.Empty(arena.Prison.GetInmates()); await arena.StopAsync(CancellationToken.None); }
public async Task AlicesSpentAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5, MaxSuggestedAmountBase = Money.Satoshis(ProtocolConstants.MaxAmountPerAlice) }; var(keyChain, coin1, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); mockRpc.Setup(rpc => rpc.SendRawTransactionAsync(It.IsAny <Transaction>(), It.IsAny <CancellationToken>())) .ThrowsAsync(new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null)); Prison prison = new(); using Arena arena = await ArenaBuilder.From(cfg, mockRpc, prison).CreateAndStartAsync(); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, keyChain, coin1, coin2); await aliceClient1.ReadyToSignAsync(CancellationToken.None); await aliceClient2.ReadyToSignAsync(CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await aliceClient2.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.Rounds.Where(x => x.Phase != Phase.Ended)); Assert.Equal(Phase.Ended, round.Phase); Assert.False(round.WasTransactionBroadcast); // There should be no inmate, because we aren't punishing spenders with banning // as there's no reason to ban already spent UTXOs, // the cost of spending the UTXO is the punishment instead. Assert.Empty(prison.GetInmates()); await arena.StopAsync(CancellationToken.None); }
public async Task CreateNewAsync() { var config = new WabiSabiConfig { MaxInputCountByRound = 1 }; var round = WabiSabiFactory.CreateRound(config); var km = ServiceFactory.CreateKeyManager(""); var key = BitcoinFactory.CreateHdPubKey(km); SmartCoin coin1 = BitcoinFactory.CreateSmartCoin(key, Money.Coins(1m)); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, mockRpc, round); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); var wabiSabiApi = new WabiSabiController(coordinator); var insecureRandom = new InsecureRandom(); var roundState = RoundState.FromRound(round); var arenaClient = new ArenaClient( roundState.CreateAmountCredentialClient(insecureRandom), roundState.CreateVsizeCredentialClient(insecureRandom), wabiSabiApi); Assert.Equal(Phase.InputRegistration, arena.Rounds.First().Phase); var bitcoinSecret = km.GetSecrets("", coin1.ScriptPubKey).Single().PrivateKey.GetBitcoinSecret(Network.Main); var aliceClient = new AliceClient(round.Id, arenaClient, coin1.Coin, round.FeeRate, bitcoinSecret); await aliceClient.RegisterInputAsync(CancellationToken.None); using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), wabiSabiApi); Task confirmationTask = aliceClient.ConfirmConnectionAsync( TimeSpan.FromSeconds(1), new long[] { coin1.EffectiveValue(round.FeeRate) }, new long[] { roundState.MaxVsizeAllocationPerAlice - coin1.ScriptPubKey.EstimateInputVsize() }, roundStateUpdater, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); await confirmationTask; Assert.Equal(Phase.ConnectionConfirmation, arena.Rounds.First().Phase); }
public async Task SomeBobsRegisteredTimeoutAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5, OutputRegistrationTimeout = TimeSpan.Zero }; using Key key1 = new(); using Key key2 = new(); var coin1 = WabiSabiFactory.CreateCoin(key1); var coin2 = WabiSabiFactory.CreateCoin(key2); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1, coin2); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc).ConfigureAwait(false); var(round, arenaClient, alices) = await CreateRoundWithTwoConfirmedConnectionsAsync(arena, key1, coin1, key2, coin2).ConfigureAwait(false); var(realAmountCredentials1, realVsizeCredentials1) = alices[0]; var(realAmountCredentials2, realVsizeCredentials2) = alices[1]; // Register outputs. var bobClient = new BobClient(round.Id, arenaClient); using var destKey = new Key(); await bobClient.RegisterOutputAsync( coin1.Amount - round.FeeRate.GetFee(coin1.ScriptPubKey.EstimateInputVsize()), destKey.PubKey.WitHash.ScriptPubKey, realAmountCredentials1, realVsizeCredentials1, CancellationToken.None).ConfigureAwait(false); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var tx = round.Assert <SigningState>().CreateTransaction(); Assert.Equal(2, tx.Inputs.Count); Assert.Equal(2, tx.Outputs.Count); Assert.Contains(cfg.BlameScript, tx.Outputs.Select(x => x.ScriptPubKey)); await arena.StopAsync(CancellationToken.None); }
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 resp = await arenaClient.RegisterInputAsync(round.Id, coin.Outpoint, key, CancellationToken.None); AssertSingleAliceSuccessfullyRegistered(round, minAliceDeadline, resp); await arena.StopAsync(CancellationToken.None); }
public async Task TimeoutInsufficientPeersAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 1, TransactionSigningTimeout = TimeSpan.Zero }; using Key key1 = new(); using Key key2 = new(); var coin1 = WabiSabiFactory.CreateCoin(key1); var coin2 = WabiSabiFactory.CreateCoin(key2); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1, coin2); mockRpc.Setup(rpc => rpc.SendRawTransactionAsync(It.IsAny <Transaction>())) .ThrowsAsync(new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null)); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, key1, coin1, key2, coin2); await aliceClient1.ReadyToSignAsync(CancellationToken.None); await aliceClient2.ReadyToSignAsync(CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.ActiveRounds); Assert.Equal(Phase.Ended, round.Phase); Assert.False(round.WasTransactionBroadcast); Assert.Empty(arena.Rounds.Where(x => x.IsBlameRound)); Assert.Contains(aliceClient2.Coin.Outpoint, arena.Prison.GetInmates().Select(x => x.Utxo)); await arena.StopAsync(CancellationToken.None); }
public async Task TimeoutInsufficientPeersAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 1, TransactionSigningTimeout = TimeSpan.Zero, MaxSuggestedAmountBase = Money.Satoshis(ProtocolConstants.MaxAmountPerAlice) }; var(keyChain, coin1, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); mockRpc.Setup(rpc => rpc.SendRawTransactionAsync(It.IsAny <Transaction>(), It.IsAny <CancellationToken>())) .ThrowsAsync(new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null)); Prison prison = new(); using Arena arena = await ArenaBuilder.From(cfg, mockRpc, prison).CreateAndStartAsync(); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, keyChain, coin1, coin2); await aliceClient1.ReadyToSignAsync(CancellationToken.None); await aliceClient2.ReadyToSignAsync(CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.Rounds.Where(x => x.Phase != Phase.Ended)); Assert.Equal(Phase.Ended, round.Phase); Assert.Equal(EndRoundState.AbortedNotEnoughAlicesSigned, round.EndRoundState); Assert.Empty(arena.Rounds.Where(x => x is BlameRound)); Assert.Contains(aliceClient2.SmartCoin.OutPoint, prison.GetInmates().Select(x => x.Utxo)); await arena.StopAsync(CancellationToken.None); }
public async Task FailsBroadcastAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; var(keyChain, coin1, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); mockRpc.Setup(rpc => rpc.SendRawTransactionAsync(It.IsAny <Transaction>(), It.IsAny <CancellationToken>())) .ThrowsAsync(new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null)); Prison prison = new(); using Arena arena = await ArenaBuilder.From(cfg, mockRpc, prison).CreateAndStartAsync(); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, keyChain, coin1, coin2); await aliceClient1.ReadyToSignAsync(CancellationToken.None); await aliceClient2.ReadyToSignAsync(CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await aliceClient2.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.Rounds.Where(x => x.Phase != Phase.Ended)); Assert.Equal(Phase.Ended, round.Phase); Assert.False(round.WasTransactionBroadcast); Assert.Empty(prison.GetInmates()); await arena.StopAsync(CancellationToken.None); }
public async Task AliceTimesoutAsync() { // Alice times out when its deadline is reached. WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); var km = ServiceFactory.CreateKeyManager(""); var key = BitcoinFactory.CreateHdPubKey(km); var smartCoin = BitcoinFactory.CreateSmartCoin(key, 10m); var rpc = WabiSabiFactory.CreatePreconfiguredRpcClient(smartCoin.Coin); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, rpc, round); var arenaClient = WabiSabiFactory.CreateArenaClient(arena); using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), arena); await roundStateUpdater.StartAsync(CancellationToken.None); // Register Alices. using var identificationKey = new Key(); var esk = km.GetSecrets("", smartCoin.ScriptPubKey).Single(); using CancellationTokenSource cancellationTokenSource = new(); var task = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, smartCoin, esk.PrivateKey.GetBitcoinSecret(round.Network), identificationKey, roundStateUpdater, cancellationTokenSource.Token); while (round.Alices.Count == 0) { await Task.Delay(10); } var alice = Assert.Single(round.Alices); alice.Deadline = DateTimeOffset.UtcNow - TimeSpan.FromMilliseconds(1); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Empty(round.Alices); cancellationTokenSource.Cancel(); await Assert.ThrowsAsync <OperationCanceledException>(async() => await task); await roundStateUpdater.StopAsync(CancellationToken.None); await arena.StopAsync(CancellationToken.None); }
public async Task SomeBobsRegisteredTimeoutAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5, OutputRegistrationTimeout = TimeSpan.Zero, CoordinationFeeRate = CoordinationFeeRate.Zero }; var(keyChain, coin1, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); using Arena arena = await ArenaBuilder.From(cfg).With(mockRpc).CreateAndStartAsync(); var(round, arenaClient, alices) = await CreateRoundWithTwoConfirmedConnectionsAsync(arena, keyChain, coin1, coin2); var(amountCredentials1, vsizeCredentials1) = (alices[0].IssuedAmountCredentials, alices[0].IssuedVsizeCredentials); var(amountCredentials2, vsizeCredentials2) = (alices[1].IssuedAmountCredentials, alices[1].IssuedVsizeCredentials); // Register outputs. var bobClient = new BobClient(round.Id, arenaClient); using var destKey = new Key(); await bobClient.RegisterOutputAsync( destKey.PubKey.WitHash.ScriptPubKey, amountCredentials1.Take(ProtocolConstants.CredentialNumber), vsizeCredentials1.Take(ProtocolConstants.CredentialNumber), CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var tx = round.Assert <SigningState>().CreateTransaction(); Assert.Equal(2, tx.Inputs.Count); Assert.Equal(2, tx.Outputs.Count); Assert.Contains(round.CoordinatorScript, tx.Outputs.Select(x => x.ScriptPubKey)); await arena.StopAsync(CancellationToken.None); }
public async Task InputNotWhitelistedAsync() { WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); round.Alices.Add(WabiSabiFactory.CreateAlice(round)); Round blameRound = WabiSabiFactory.CreateBlameRound(round, cfg); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, round, blameRound); using Key key = new(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(); var req = WabiSabiFactory.CreateInputRegistrationRequest(round: blameRound); var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(async() => await arena.RegisterInputAsync(req, CancellationToken.None)); Assert.Equal(WabiSabiProtocolErrorCode.InputNotWhitelisted, ex.ErrorCode); await arena.StopAsync(CancellationToken.None); }
public async Task EveryoneSignedAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5, MaxSuggestedAmountBase = Money.Satoshis(ProtocolConstants.MaxAmountPerAlice) }; var(keyChain, coin1, coin2) = WabiSabiFactory.CreateCoinKeyPairs(); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin, coin2.Coin); using Arena arena = await ArenaBuilder.From(cfg).With(mockRpc).CreateAndStartAsync(); var(round, aliceClient1, aliceClient2) = await CreateRoundWithOutputsReadyToSignAsync(arena, keyChain, coin1, coin2); await aliceClient1.ReadyToSignAsync(CancellationToken.None); await aliceClient2.ReadyToSignAsync(CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Assert <SigningState>().CreateTransaction(); await aliceClient1.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await aliceClient2.SignTransactionAsync(signedCoinJoin, keyChain, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.GetActiveRounds()); Assert.Equal(Phase.Ended, round.Phase); await arena.StopAsync(CancellationToken.None); }
public async Task InputWhitelistedAsync() { var alice = WabiSabiFactory.CreateAlice(); WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); round.Alices.Add(alice); Round blameRound = WabiSabiFactory.CreateBlameRound(round, cfg); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, round, blameRound); await using ArenaRequestHandler handler = new(cfg, new(), arena, WabiSabiFactory.CreatePreconfiguredRpcClient().Object); var req = WabiSabiFactory.CreateInputRegistrationRequest(prevout: alice.Coin.Outpoint, round: blameRound); var ex = await Assert.ThrowsAnyAsync <Exception>(async() => await handler.RegisterInputAsync(req, CancellationToken.None)); if (ex is WabiSabiProtocolException wspex) { Assert.NotEqual(WabiSabiProtocolErrorCode.InputNotWhitelisted, wspex.ErrorCode); } await arena.StopAsync(CancellationToken.None); }
public async Task RoundNotFoundAsync() { using Key key = new(); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(new(), WabiSabiFactory.CreatePreconfiguredRpcClient()); 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); }