public async Task InputUnconfirmedAsync() { using Key key = new(); WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); var mockRpc = new Mock <IRPCClient>(); mockRpc.Setup(rpc => rpc.GetTxOutAsync(It.IsAny <uint256>(), It.IsAny <int>(), It.IsAny <bool>())) .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse { Confirmations = 0 }); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc, round); var arenaClient = WabiSabiFactory.CreateArenaClient(arena); var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>( async() => await arenaClient.RegisterInputAsync(round.Id, BitcoinFactory.CreateOutPoint(), key, CancellationToken.None)); Assert.Equal(WabiSabiProtocolErrorCode.InputUnconfirmed, ex.ErrorCode); await arena.StopAsync(CancellationToken.None); }
public async Task IncorrectRequestedAmountCredentialsAsync() { WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); round.SetPhase(Phase.ConnectionConfirmation); var alice = WabiSabiFactory.CreateAlice(); round.Alices.Add(alice); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, round); var incorrectAmountCredentials = WabiSabiFactory.CreateRealCredentialRequests(round, Money.Coins(3), null).amountRequest; var req = WabiSabiFactory.CreateConnectionConfirmationRequest(round) with { RealAmountCredentialRequests = incorrectAmountCredentials }; await using ArenaRequestHandler handler = new(cfg, new Prison(), arena, new MockRpcClient()); var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>(async() => await handler.ConfirmConnectionAsync(req, CancellationToken.None)); Assert.Equal(WabiSabiProtocolErrorCode.IncorrectRequestedAmountCredentials, ex.ErrorCode); Assert.False(alice.ConfirmedConnection); await arena.StopAsync(CancellationToken.None); }
public async Task RemoveInputAsyncTest() { var config = new WabiSabiConfig(); var round = WabiSabiFactory.CreateRound(config); round.SetPhase(Phase.ConnectionConfirmation); var fundingTx = BitcoinFactory.CreateSmartTransaction(ownOutputCount: 1); var coin = fundingTx.WalletOutputs.First().Coin; var alice = new Alice(new Dictionary <Coin, byte[]> { { coin, Array.Empty <byte>() } }); round.Alices.Add(alice); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, arena.Rpc); var apiClient = new ArenaClient(null !, null !, coordinator); round.SetPhase(Phase.InputRegistration); await apiClient.RemoveInputAsync(round.Id, alice.Id); Assert.Empty(round.Alices); }
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(); cfg.StandardInputRegistrationTimeout = TimeSpan.Zero; var arenaClient = WabiSabiFactory.CreateArenaClient(arena); var ex = await Assert.ThrowsAsync <WabiSabiProtocolException>( async() => await arenaClient.RegisterInputAsync(round.Id, coin.Outpoint, key, CancellationToken.None)); Assert.Equal(WabiSabiProtocolErrorCode.WrongPhase, ex.ErrorCode); Assert.Equal(Phase.InputRegistration, round.Phase); await arena.StopAsync(CancellationToken.None); }
public async Task AlicesSpentAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; var mockRpc = new MockRpcClient(); mockRpc.OnSendRawTransactionAsync = _ => throw new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null); mockRpc.OnGetTxOutAsync ??= (_, _, _) => null; using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc); // Create the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var round = Assert.Single(arena.Rounds).Value; // Register Alices. using Key key1 = new(); using Key key2 = new(); var irreq1 = WabiSabiFactory.CreateInputsRegistrationRequest(key1, round); var irres1 = await arena.RegisterInputAsync( irreq1.RoundId, irreq1.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key1.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq1.ZeroAmountCredentialRequests, irreq1.ZeroWeightCredentialRequests); var irreq2 = WabiSabiFactory.CreateInputsRegistrationRequest(key2, round); var irres2 = await arena.RegisterInputAsync( irreq2.RoundId, irreq2.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key2.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq2.ZeroAmountCredentialRequests, irreq2.ZeroWeightCredentialRequests); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.ConnectionConfirmation, round.Phase); var alice1 = round.Alices.Single(x => x.Id == irres1.AliceId); var alice2 = round.Alices.Single(x => x.Id == irres2.AliceId); // Confirm connections. var ccresps = new List <(ConnectionConfirmationResponse resp, WabiSabiClient amountClient, WabiSabiClient weightClient, Guid aliceId)>(); var ccreq1 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres1); var ccresp1 = await arena.ConfirmConnectionAsync(ccreq1.request); ccresps.Add((ccresp1, ccreq1.amountClient, ccreq1.weightClient, irres2.AliceId)); ccreq1.amountClient.HandleResponse(ccresp1.RealAmountCredentials !, ccreq1.amountValidation); ccreq1.weightClient.HandleResponse(ccresp1.RealWeightCredentials !, ccreq1.weightValidation); var ccreq2 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres2); var ccresp2 = await arena.ConfirmConnectionAsync(ccreq2.request); ccresps.Add((ccresp2, ccreq2.amountClient, ccreq2.weightClient, irres1.AliceId)); ccreq2.amountClient.HandleResponse(ccresp2.RealAmountCredentials !, ccreq2.amountValidation); ccreq2.weightClient.HandleResponse(ccresp2.RealWeightCredentials !, ccreq2.weightValidation); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); // Register outputs. foreach (var orreq in WabiSabiFactory.CreateOutputRegistrationRequests(round, ccresps)) { var orresp = await arena.RegisterOutputAsync(orreq); } await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Coinjoin.Clone(); var coin1 = alice1.Coins.First(); var coin2 = alice2.Coins.First(); var idx1 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin1.Outpoint)); var idx2 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin2.Outpoint)); signedCoinJoin.Sign(key1.GetBitcoinSecret(Network.Main), coin1); var txsigreq1 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx1, signedCoinJoin.Inputs[idx1].WitScript) }); signedCoinJoin.Sign(key2.GetBitcoinSecret(Network.Main), coin2); var txsigreq2 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx2, signedCoinJoin.Inputs[idx2].WitScript) }); await arena.SignTransactionAsync(txsigreq1); await arena.SignTransactionAsync(txsigreq2); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round.Id, arena.Rounds.Keys); // 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 RegisterInputAsyncTest() { var config = new WabiSabiConfig(); var round = WabiSabiFactory.CreateRound(config); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round); 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)) .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse { IsCoinBase = false, Confirmations = 200, TxOut = new TxOut(Money.Coins(1m), key.PubKey.WitHash.GetAddress(Network.Main)), }); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); var rnd = new InsecureRandom(); var protocolCredentialNumber = 2; var protocolMaxWeightPerAlice = 1_000L; var amountClient = new WabiSabiClient(round.AmountCredentialIssuerParameters, protocolCredentialNumber, rnd, 4_300_000_000_000ul); var weightClient = new WabiSabiClient(round.WeightCredentialIssuerParameters, protocolCredentialNumber, rnd, (ulong)protocolMaxWeightPerAlice); var apiClient = new ArenaClient(amountClient, weightClient, coordinator); var aliceId = await apiClient.RegisterInputAsync(Money.Coins(1m), outpoint, key, round.Id, round.Hash); Assert.NotEqual(Guid.Empty, aliceId); Assert.Empty(apiClient.AmountCredentialClient.Credentials.Valuable); var reissuanceAmounts = new[] { Money.Coins(.75m) - round.FeeRate.GetFee(Constants.P2wpkhInputVirtualSize), Money.Coins(.25m) }; var inputWeight = 4 * Constants.P2wpkhInputVirtualSize; var inputRemainingWeights = new[] { protocolMaxWeightPerAlice - inputWeight }; // Phase: Input Registration await apiClient.ConfirmConnectionAsync( round.Id, aliceId, inputRemainingWeights, apiClient.AmountCredentialClient.Credentials.ZeroValue.Take(protocolCredentialNumber), reissuanceAmounts); Assert.Empty(apiClient.AmountCredentialClient.Credentials.Valuable); // Phase: Connection Confirmation round.SetPhase(Phase.ConnectionConfirmation); await apiClient.ConfirmConnectionAsync( round.Id, aliceId, inputRemainingWeights, apiClient.AmountCredentialClient.Credentials.ZeroValue.Take(protocolCredentialNumber), reissuanceAmounts); Assert.Single(apiClient.AmountCredentialClient.Credentials.Valuable, x => x.Amount.ToMoney() == reissuanceAmounts.First()); Assert.Single(apiClient.AmountCredentialClient.Credentials.Valuable, x => x.Amount.ToMoney() == reissuanceAmounts.Last()); }
public async Task SomeBobsRegisteredTimeoutAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5, OutputRegistrationTimeout = TimeSpan.Zero }; using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg); // Create the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var round = Assert.Single(arena.Rounds).Value; // Register Alices. using Key key1 = new(); var irreq1 = WabiSabiFactory.CreateInputsRegistrationRequest(key1, round); var irres1 = await arena.RegisterInputAsync( irreq1.RoundId, irreq1.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key1.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq1.ZeroAmountCredentialRequests, irreq1.ZeroVsizeCredentialRequests); using Key key2 = new(); var irreq2 = WabiSabiFactory.CreateInputsRegistrationRequest(key2, round); var irres2 = await arena.RegisterInputAsync( irreq2.RoundId, irreq2.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key2.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq2.ZeroAmountCredentialRequests, irreq2.ZeroVsizeCredentialRequests); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.ConnectionConfirmation, round.Phase); // Confirm connections. var ccresps = new List <(ConnectionConfirmationResponse resp, WabiSabiClient amountClient, WabiSabiClient vsizeClient, Guid aliceId)>(); var ccreq1 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres1); var ccresp1 = await arena.ConfirmConnectionAsync(ccreq1.request); ccresps.Add((ccresp1, ccreq1.amountClient, ccreq1.vsizeClient, irres2.AliceId)); ccreq1.amountClient.HandleResponse(ccresp1.RealAmountCredentials !, ccreq1.amountValidation); ccreq1.vsizeClient.HandleResponse(ccresp1.RealVsizeCredentials !, ccreq1.vsizeValidation); var ccreq2 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres2); var ccresp2 = await arena.ConfirmConnectionAsync(ccreq2.request); ccresps.Add((ccresp2, ccreq2.amountClient, ccreq2.vsizeClient, irres1.AliceId)); ccreq2.amountClient.HandleResponse(ccresp2.RealAmountCredentials !, ccreq2.amountValidation); ccreq2.vsizeClient.HandleResponse(ccresp2.RealVsizeCredentials !, ccreq2.vsizeValidation); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); // Register outputs. var orresp = await arena.RegisterOutputAsync(WabiSabiFactory.CreateOutputRegistrationRequests(round, ccresps).First()); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); Assert.Equal(2, round.Coinjoin.Inputs.Count); Assert.Equal(2, round.Coinjoin.Outputs.Count); Assert.Contains(cfg.BlameScript, round.Coinjoin.Outputs.Select(x => x.ScriptPubKey)); await arena.StopAsync(CancellationToken.None); }
public async Task FullCoinjoinAsyncTestAsync() { var config = new WabiSabiConfig { MaxInputCountByRound = 1 }; var round = WabiSabiFactory.CreateRound(config); round.MaxVsizeAllocationPerAlice = 255; 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)) .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>())) .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()).Returns(Task.CompletedTask); 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 aliceArenaClient = new ArenaClient( roundState.CreateAmountCredentialClient(insecureRandom), roundState.CreateVsizeCredentialClient(insecureRandom), wabiSabiApi); var inputRegistrationResponse = await aliceArenaClient.RegisterInputAsync(round.Id, outpoint, key, CancellationToken.None); var aliceId = inputRegistrationResponse.Value; var inputVsize = Constants.P2wpkhInputVirtualSize; var amountsToRequest = new[] { Money.Coins(.75m) - round.FeeRate.GetFee(inputVsize), 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[] { roundState.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, tx.Outputs.Count); }
public async Task SignTransactionAsync() { WabiSabiConfig config = new(); Round round = WabiSabiFactory.CreateRound(config); using Key key1 = new(); Alice alice1 = WabiSabiFactory.CreateAlice(key: key1, round: round); round.Alices.Add(alice1); using Key key2 = new(); Alice alice2 = WabiSabiFactory.CreateAlice(key: key2, round: round); round.Alices.Add(alice2); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round); var mockRpc = new Mock <IRPCClient>(); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); var wabiSabiApi = new WabiSabiController(coordinator); var rnd = new InsecureRandom(); var amountClient = new WabiSabiClient(round.AmountCredentialIssuerParameters, rnd, 4300000000000L); var vsizeClient = new WabiSabiClient(round.VsizeCredentialIssuerParameters, rnd, 2000L); var apiClient = new ArenaClient(amountClient, vsizeClient, wabiSabiApi); round.SetPhase(Phase.TransactionSigning); var emptyState = round.Assert <ConstructionState>(); // We can't use ``emptyState.Finalize()` because this is not a valid transaction so we fake it var finalizedEmptyState = new SigningState(emptyState.Parameters, emptyState.Inputs, emptyState.Outputs); // No inputs in the CoinJoin. await Assert.ThrowsAsync <ArgumentException>(async() => await apiClient.SignTransactionAsync(round.Id, alice1.Coin, new BitcoinSecret(key1, Network.Main), finalizedEmptyState.CreateUnsignedTransaction(), CancellationToken.None)); var oneInput = emptyState.AddInput(alice1.Coin).Finalize(); round.CoinjoinState = oneInput; // Trying to sign coins those are not in the CoinJoin. await Assert.ThrowsAsync <InvalidOperationException>(async() => await apiClient.SignTransactionAsync(round.Id, alice2.Coin, new BitcoinSecret(key2, Network.Main), oneInput.CreateUnsignedTransaction(), CancellationToken.None)); var twoInputs = emptyState.AddInput(alice1.Coin).AddInput(alice2.Coin).Finalize(); round.CoinjoinState = twoInputs; // Trying to sign coins with the wrong secret. await Assert.ThrowsAsync <InvalidOperationException>(async() => await apiClient.SignTransactionAsync(round.Id, alice1.Coin, new BitcoinSecret(key2, Network.Main), twoInputs.CreateUnsignedTransaction(), CancellationToken.None)); Assert.False(round.Assert <SigningState>().IsFullySigned); var unsigned = round.Assert <SigningState>().CreateUnsignedTransaction(); await apiClient.SignTransactionAsync(round.Id, alice1.Coin, new BitcoinSecret(key1, Network.Main), unsigned, CancellationToken.None); Assert.True(round.Assert <SigningState>().IsInputSigned(alice1.Coin.Outpoint)); Assert.False(round.Assert <SigningState>().IsInputSigned(alice2.Coin.Outpoint)); Assert.False(round.Assert <SigningState>().IsFullySigned); await apiClient.SignTransactionAsync(round.Id, alice2.Coin, new BitcoinSecret(key2, Network.Main), unsigned, CancellationToken.None); Assert.True(round.Assert <SigningState>().IsInputSigned(alice2.Coin.Outpoint)); Assert.True(round.Assert <SigningState>().IsFullySigned); }
public async Task TimeoutSufficientPeersAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 1, TransactionSigningTimeout = TimeSpan.Zero, 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); 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); // 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, CancellationToken.None); await aliceClient2.SignTransactionAsync(signedCoinJoin, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round, arena.ActiveRounds); Assert.Single(arena.Rounds.Where(x => x.IsBlameRound)); var badOutpoint = alice3.Coin.Outpoint; Assert.Contains(badOutpoint, arena.Prison.GetInmates().Select(x => x.Utxo)); var blameRound = arena.Rounds.Single(x => x.IsBlameRound); Assert.True(blameRound.IsBlameRound); Assert.NotNull(blameRound.BlameOf); Assert.Equal(round.Id, blameRound.BlameOf?.Id); var whitelist = blameRound.BlameWhitelist; Assert.Contains(aliceClient1.Coin.Outpoint, whitelist); Assert.Contains(aliceClient2.Coin.Outpoint, whitelist); Assert.DoesNotContain(badOutpoint, whitelist); await arena.StopAsync(CancellationToken.None); }
public async Task DiffTooSmallToBlameAsync() { 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); 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(); using var destKey2 = 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); 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); // 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(cfg.BlameScript, tx.Outputs.Select(x => x.ScriptPubKey)); await arena.StopAsync(CancellationToken.None); }
public async Task RegisterOutputTestAsync() { 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(2m)); 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 insecureRandom = new InsecureRandom(); var wabiSabiApi = new WabiSabiController(coordinator); var roundState = RoundState.FromRound(round); var aliceArenaClient = new ArenaClient( roundState.CreateAmountCredentialClient(insecureRandom), roundState.CreateVsizeCredentialClient(insecureRandom), wabiSabiApi); var bobArenaClient = new ArenaClient( roundState.CreateAmountCredentialClient(insecureRandom), roundState.CreateVsizeCredentialClient(insecureRandom), wabiSabiApi); Assert.Equal(Phase.InputRegistration, round.Phase); var bitcoinSecret = km.GetSecrets("", coin1.ScriptPubKey).Single().PrivateKey.GetBitcoinSecret(Network.Main); using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), wabiSabiApi); await roundStateUpdater.StartAsync(CancellationToken.None); var task = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), aliceArenaClient, coin1, bitcoinSecret, roundStateUpdater, CancellationToken.None); do { await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); }while (round.Phase != Phase.ConnectionConfirmation); var aliceClient = await task; await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); Assert.Equal(Phase.OutputRegistration, round.Phase); using var destinationKey = new Key(); var destination = destinationKey.PubKey.WitHash.ScriptPubKey; var bobClient = new BobClient(round.Id, bobArenaClient); await bobClient.RegisterOutputAsync( destination, aliceClient.IssuedAmountCredentials.Take(ProtocolConstants.CredentialNumber), aliceClient.IssuedVsizeCredentials.Take(ProtocolConstants.CredentialNumber), CancellationToken.None); var bob = Assert.Single(round.Bobs); Assert.Equal(destination, bob.Script); var credentialAmountSum = aliceClient.IssuedAmountCredentials.Take(ProtocolConstants.CredentialNumber).Sum(x => x.Value); Assert.Equal(credentialAmountSum, bob.CredentialAmount); }
public async Task RegisterOutputTestAsync() { var config = new WabiSabiConfig { MaxInputCountByRound = 1 }; var round = WabiSabiFactory.CreateRound(config); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); var km = ServiceFactory.CreateKeyManager(""); var key = BitcoinFactory.CreateHdPubKey(km); SmartCoin coin1 = BitcoinFactory.CreateSmartCoin(key, Money.Coins(2m)); var outpoint = coin1.OutPoint; var mockRpc = new Mock <IRPCClient>(); mockRpc.Setup(rpc => rpc.GetTxOutAsync(outpoint.Hash, (int)outpoint.N, true)) .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse { IsCoinBase = false, Confirmations = coin1.Height, TxOut = coin1.TxOut, }); CredentialPool amountCredential = new(); CredentialPool vsizeCredential = new(); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); var aliceArenaClient = new ArenaClient(round.AmountCredentialIssuerParameters, round.VsizeCredentialIssuerParameters, amountCredential, vsizeCredential, coordinator, new InsecureRandom()); var bobArenaClient = new ArenaClient(round.AmountCredentialIssuerParameters, round.VsizeCredentialIssuerParameters, amountCredential, vsizeCredential, coordinator, new InsecureRandom()); Assert.Equal(Phase.InputRegistration, round.Phase); var bitcoinSecret = km.GetSecrets("", coin1.ScriptPubKey).Single().PrivateKey.GetBitcoinSecret(Network.Main); var aliceClient = await AliceClient.CreateNewAsync(aliceArenaClient, new[] { coin1.Coin }, bitcoinSecret, round.Id, round.Hash, round.FeeRate); Task confirmationTask = aliceClient.ConfirmConnectionAsync(TimeSpan.FromSeconds(3), CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); await confirmationTask; Assert.Equal(Phase.ConnectionConfirmation, round.Phase); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); Assert.Equal(Phase.OutputRegistration, round.Phase); using var destinationKey1 = new Key(); using var destinationKey2 = new Key(); using var destinationKey3 = new Key(); using var destinationKey4 = new Key(); var bobClient = new BobClient(round.Id, bobArenaClient); await bobClient.RegisterOutputAsync(Money.Coins(0.25m), destinationKey1.PubKey.WitHash.ScriptPubKey); await bobClient.RegisterOutputAsync(Money.Coins(0.25m), destinationKey2.PubKey.WitHash.ScriptPubKey); await bobClient.RegisterOutputAsync(Money.Coins(0.25m), destinationKey3.PubKey.WitHash.ScriptPubKey); await bobClient.RegisterOutputAsync(Money.Coins(0.25m), destinationKey4.PubKey.WitHash.ScriptPubKey); Assert.Equal(4, round.Bobs.Count); }
public async Task RegisterOutputTestAsync() { 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(2m)); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, mockRpc, round); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); ZeroCredentialPool zeroAmountCredential = new(); ZeroCredentialPool zeroVsizeCredential = new(); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); var insecureRandom = new InsecureRandom(); var wabiSabiApi = new WabiSabiController(coordinator); var roundState = RoundState.FromRound(round); var aliceArenaClient = new ArenaClient( roundState.CreateAmountCredentialClient(zeroAmountCredential, insecureRandom), roundState.CreateVsizeCredentialClient(zeroVsizeCredential, insecureRandom), wabiSabiApi); var bobArenaClient = new ArenaClient( roundState.CreateAmountCredentialClient(zeroAmountCredential, insecureRandom), roundState.CreateVsizeCredentialClient(zeroVsizeCredential, insecureRandom), wabiSabiApi); Assert.Equal(Phase.InputRegistration, round.Phase); var bitcoinSecret = km.GetSecrets("", coin1.ScriptPubKey).Single().PrivateKey.GetBitcoinSecret(Network.Main); var aliceClient = new AliceClient(round.Id, aliceArenaClient, coin1.Coin, round.FeeRate, bitcoinSecret); await aliceClient.RegisterInputAsync(CancellationToken.None); Task confirmationTask = aliceClient.ConfirmConnectionAsync(TimeSpan.FromSeconds(1), roundState.MaxVsizeAllocationPerAlice, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); await confirmationTask; Assert.Equal(Phase.ConnectionConfirmation, round.Phase); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); Assert.Equal(Phase.OutputRegistration, round.Phase); using var destinationKey = new Key(); var destination = destinationKey.PubKey.WitHash.ScriptPubKey; var bobClient = new BobClient(round.Id, bobArenaClient); await bobClient.RegisterOutputAsync(Money.Coins(0.25m), destination, aliceClient.RealAmountCredentials, aliceClient.RealVsizeCredentials, CancellationToken.None); var bob = Assert.Single(round.Bobs); Assert.Equal(destination, bob.Script); Assert.Equal(25_000_000, bob.CredentialAmount); }
public async Task DoesntSwitchImmaturelyAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg); // Create the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var round = Assert.Single(arena.Rounds).Value; // Register Alices. using Key key1 = new(); var irreq1 = WabiSabiFactory.CreateInputsRegistrationRequest(key1, round); var irres1 = await arena.RegisterInputAsync( irreq1.RoundId, irreq1.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key1.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq1.ZeroAmountCredentialRequests, irreq1.ZeroWeightCredentialRequests); using Key key2 = new(); var irreq2 = WabiSabiFactory.CreateInputsRegistrationRequest(key2, round); var irres2 = await arena.RegisterInputAsync( irreq2.RoundId, irreq2.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key2.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq2.ZeroAmountCredentialRequests, irreq2.ZeroWeightCredentialRequests); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.ConnectionConfirmation, round.Phase); // Confirm connections. var ccresps = new List <(ConnectionConfirmationResponse resp, WabiSabiClient amountClient, WabiSabiClient weightClient, Guid aliceId)>(); var ugliSolution = false; foreach (var ccreq in WabiSabiFactory.CreateConnectionConfirmationRequests(round, irres1, irres2)) { var ccresp = await arena.ConfirmConnectionAsync(ccreq.request); ccresps.Add((ccresp, ccreq.amountClient, ccreq.weightClient, ugliSolution ? irres1.AliceId : irres2.AliceId)); ugliSolution = true; ccreq.amountClient.HandleResponse(ccresp.RealAmountCredentials !, ccreq.amountValidation); ccreq.weightClient.HandleResponse(ccresp.RealWeightCredentials !, ccreq.weightValidation); } await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); // Register outputs. var orresp = await arena.RegisterOutputAsync(WabiSabiFactory.CreateOutputRegistrationRequests(round, ccresps).First()); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); await arena.StopAsync(CancellationToken.None); } }
public async Task EveryoneSignedAsync() { WabiSabiConfig cfg = new() { MaxInputCountByRound = 2, MinInputCountByRoundMultiplier = 0.5 }; using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg); // Create the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var round = Assert.Single(arena.Rounds).Value; // Register Alices. using Key key1 = new(); using Key key2 = new(); var irreq1 = WabiSabiFactory.CreateInputsRegistrationRequest(key1, round); var irres1 = await arena.RegisterInputAsync( irreq1.RoundId, irreq1.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key1.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq1.ZeroAmountCredentialRequests, irreq1.ZeroWeightCredentialRequests); var irreq2 = WabiSabiFactory.CreateInputsRegistrationRequest(key2, round); var irres2 = await arena.RegisterInputAsync( irreq2.RoundId, irreq2.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key2.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq2.ZeroAmountCredentialRequests, irreq2.ZeroWeightCredentialRequests); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.ConnectionConfirmation, round.Phase); var alice1 = round.Alices.Single(x => x.Id == irres1.AliceId); var alice2 = round.Alices.Single(x => x.Id == irres2.AliceId); // Confirm connections. var ccresps = new List <(ConnectionConfirmationResponse resp, WabiSabiClient amountClient, WabiSabiClient weightClient, Guid aliceId)>(); var ccreq1 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres1); var ccresp1 = await arena.ConfirmConnectionAsync(ccreq1.request); ccresps.Add((ccresp1, ccreq1.amountClient, ccreq1.weightClient, irres2.AliceId)); ccreq1.amountClient.HandleResponse(ccresp1.RealAmountCredentials !, ccreq1.amountValidation); ccreq1.weightClient.HandleResponse(ccresp1.RealWeightCredentials !, ccreq1.weightValidation); var ccreq2 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres2); var ccresp2 = await arena.ConfirmConnectionAsync(ccreq2.request); ccresps.Add((ccresp2, ccreq2.amountClient, ccreq2.weightClient, irres1.AliceId)); ccreq2.amountClient.HandleResponse(ccresp2.RealAmountCredentials !, ccreq2.amountValidation); ccreq2.weightClient.HandleResponse(ccresp2.RealWeightCredentials !, ccreq2.weightValidation); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); // Register outputs. foreach (var orreq in WabiSabiFactory.CreateOutputRegistrationRequests(round, ccresps)) { var orresp = await arena.RegisterOutputAsync(orreq); } await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Coinjoin.Clone(); var coin1 = alice1.Coins.First(); var coin2 = alice2.Coins.First(); var idx1 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin1.Outpoint)); var idx2 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin2.Outpoint)); signedCoinJoin.Sign(key1.GetBitcoinSecret(Network.Main), coin1); var txsigreq1 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx1, signedCoinJoin.Inputs[idx1].WitScript) }); signedCoinJoin.Sign(key2.GetBitcoinSecret(Network.Main), coin2); var txsigreq2 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx2, signedCoinJoin.Inputs[idx2].WitScript) }); await arena.SignTransactionAsync(txsigreq1); await arena.SignTransactionAsync(txsigreq2); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionBroadcasting, 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 mockRpc = new MockRpcClient(); mockRpc.OnSendRawTransactionAsync = _ => throw new RPCException(RPCErrorCode.RPC_TRANSACTION_REJECTED, "", null); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(cfg, mockRpc); // Create the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var round = Assert.Single(arena.Rounds).Value; // Register Alices. using Key key1 = new(); using Key key2 = new(); var irreq1 = WabiSabiFactory.CreateInputsRegistrationRequest(key1, round); var irres1 = await arena.RegisterInputAsync( irreq1.RoundId, irreq1.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key1.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq1.ZeroAmountCredentialRequests, irreq1.ZeroWeightCredentialRequests); var irreq2 = WabiSabiFactory.CreateInputsRegistrationRequest(key2, round); var irres2 = await arena.RegisterInputAsync( irreq2.RoundId, irreq2.InputRoundSignaturePairs.ToDictionary(x => new Coin(x.Input, new TxOut(Money.Coins(1), key2.PubKey.GetSegwitAddress(Network.Main))), x => x.RoundSignature), irreq2.ZeroAmountCredentialRequests, irreq2.ZeroWeightCredentialRequests); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.ConnectionConfirmation, round.Phase); var alice1 = round.Alices.Single(x => x.Id == irres1.AliceId); var alice2 = round.Alices.Single(x => x.Id == irres2.AliceId); // Confirm connections. var ccresps = new List <(ConnectionConfirmationResponse resp, WabiSabiClient amountClient, WabiSabiClient weightClient, Guid aliceId)>(); var ccreq1 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres1); var ccresp1 = await arena.ConfirmConnectionAsync(ccreq1.request); ccresps.Add((ccresp1, ccreq1.amountClient, ccreq1.weightClient, irres2.AliceId)); ccreq1.amountClient.HandleResponse(ccresp1.RealAmountCredentials !, ccreq1.amountValidation); ccreq1.weightClient.HandleResponse(ccresp1.RealWeightCredentials !, ccreq1.weightValidation); var ccreq2 = WabiSabiFactory.CreateConnectionConfirmationRequest(round, irres2); var ccresp2 = await arena.ConfirmConnectionAsync(ccreq2.request); ccresps.Add((ccresp2, ccreq2.amountClient, ccreq2.weightClient, irres1.AliceId)); ccreq2.amountClient.HandleResponse(ccresp2.RealAmountCredentials !, ccreq2.amountValidation); ccreq2.weightClient.HandleResponse(ccresp2.RealWeightCredentials !, ccreq2.weightValidation); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); // Register outputs. foreach (var orreq in WabiSabiFactory.CreateOutputRegistrationRequests(round, ccresps)) { var orresp = await arena.RegisterOutputAsync(orreq); } // Make sure not all alices signed. var alice3 = WabiSabiFactory.CreateAlice(); alice3.ConfirmedConnection = true; round.Alices.Add(alice3); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); var signedCoinJoin = round.Coinjoin.Clone(); var coin1 = alice1.Coins.First(); var coin2 = alice2.Coins.First(); var idx1 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin1.Outpoint)); var idx2 = signedCoinJoin.Inputs.IndexOf(signedCoinJoin.Inputs.Single(x => x.PrevOut == coin2.Outpoint)); signedCoinJoin.Sign(key1.GetBitcoinSecret(Network.Main), coin1); var txsigreq1 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx1, signedCoinJoin.Inputs[idx1].WitScript) }); signedCoinJoin.Sign(key2.GetBitcoinSecret(Network.Main), coin2); var txsigreq2 = new TransactionSignaturesRequest(round.Id, new[] { new InputWitnessPair((uint)idx2, signedCoinJoin.Inputs[idx2].WitScript) }); await arena.SignTransactionAsync(txsigreq1); await arena.SignTransactionAsync(txsigreq2); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.DoesNotContain(round.Id, arena.Rounds.Keys); Assert.Single(arena.Rounds.Where(x => x.Value.IsBlameRound)); var badOutpoint = alice3.Coins.Select(x => x.Outpoint).First(); Assert.Contains(badOutpoint, arena.Prison.GetInmates().Select(x => x.Utxo)); var blameRound = arena.Rounds.Single(x => x.Value.IsBlameRound).Value; Assert.True(blameRound.IsBlameRound); Assert.NotNull(blameRound.BlameOf); Assert.Equal(round.Id, blameRound.BlameOf?.Id); var whitelist = blameRound.BlameWhitelist; Assert.Contains(alice1.Coins.Select(x => x.Outpoint).First(), whitelist); Assert.Contains(alice2.Coins.Select(x => x.Outpoint).First(), whitelist); Assert.DoesNotContain(badOutpoint, whitelist); await arena.StopAsync(CancellationToken.None); } }
public async Task FullCoinjoinAsyncTest() { var config = new WabiSabiConfig { MaxInputCountByRound = 1 }; var round = WabiSabiFactory.CreateRound(config); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(1)); 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)) .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse { IsCoinBase = false, Confirmations = 200, TxOut = new TxOut(Money.Coins(1m), key.PubKey.WitHash.GetAddress(Network.Main)), }); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); var apiClient = new ArenaClient(round.AmountCredentialIssuerParameters, round.WeightCredentialIssuerParameters, coordinator, new InsecureRandom()); var amountCredentials = apiClient.AmountCredentialClient.Credentials; var weightCredentials = apiClient.WeightCredentialClient.Credentials; var aliceId = await apiClient.RegisterInputAsync(Money.Coins(1m), outpoint, key, round.Id, round.Hash); Assert.NotEqual(Guid.Empty, aliceId); Assert.Empty(amountCredentials.Valuable); var reissuanceAmounts = new[] { Money.Coins(.75m) - round.FeeRate.GetFee(Constants.P2wpkhInputVirtualSize), Money.Coins(.25m) }; var inputWeight = Constants.WitnessScaleFactor * Constants.P2wpkhInputVirtualSize; var inputRemainingWeights = new[] { (long)ArenaClient.ProtocolMaxWeightPerAlice - inputWeight }; // Phase: Input Registration Assert.Equal(Phase.InputRegistration, round.Phase); await apiClient.ConfirmConnectionAsync( round.Id, aliceId, inputRemainingWeights, amountCredentials.ZeroValue.Take(ArenaClient.ProtocolCredentialNumber), reissuanceAmounts); Assert.Empty(amountCredentials.Valuable); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(1)); Assert.Equal(Phase.ConnectionConfirmation, round.Phase); // Phase: Connection Confirmation await apiClient.ConfirmConnectionAsync( round.Id, aliceId, inputRemainingWeights, amountCredentials.ZeroValue.Take(ArenaClient.ProtocolCredentialNumber), reissuanceAmounts); Assert.Single(apiClient.AmountCredentialClient.Credentials.Valuable, x => x.Amount.ToMoney() == reissuanceAmounts.First()); Assert.Single(apiClient.AmountCredentialClient.Credentials.Valuable, x => x.Amount.ToMoney() == reissuanceAmounts.Last()); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(1)); Assert.Equal(Phase.OutputRegistration, round.Phase); // Phase: Output Registration using var destinationKey1 = new Key(); using var destinationKey2 = new Key(); await apiClient.RegisterOutputAsync( round.Id, reissuanceAmounts[0], destinationKey1.PubKey.WitHash.ScriptPubKey, apiClient.AmountCredentialClient.Credentials.Valuable, apiClient.WeightCredentialClient.Credentials.Valuable); await apiClient.RegisterOutputAsync( round.Id, reissuanceAmounts[1], destinationKey2.PubKey.WitHash.ScriptPubKey, apiClient.AmountCredentialClient.Credentials.Valuable, apiClient.WeightCredentialClient.Credentials.Valuable); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.TransactionSigning, round.Phase); Assert.Equal(1, round.Coinjoin.Inputs.Count); Assert.Equal(2, round.Coinjoin.Outputs.Count); }