Refactor FullCoinjoinAsyncTestAsync
MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
@@ -31,7 +31,112 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Client;
public class ArenaClientTests
{
[Fact]
- public async Task FullCoinjoinAsyncTestAsync()
+ public async Task FullP2wpkhCoinjoinTestAsync()
+ {
+ await TestFullCoinjoinAsync(ScriptPubKeyType.Segwit, Constants.P2wpkhInputVirtualSize);
+ }
+
+ [Fact]
+ public async Task RemoveInputAsyncTestAsync()
+ {
+ 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(coin, new OwnershipProof(), round, Guid.NewGuid(), false);
+ round.Alices.Add(alice);
+
+ using Arena arena = await ArenaBuilder.From(config).CreateAndStartAsync(round);
+
+ 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 apiClient = new ArenaClient(null!, null!, config.CoordinatorIdentifier, wabiSabiApi);
+
+ round.SetPhase(Phase.InputRegistration);
+
+ await apiClient.RemoveInputAsync(round.Id, alice.Id, CancellationToken.None);
+ Assert.Empty(round.Alices);
+ }
+
+ [Fact]
+ public async Task SignTransactionAsync()
+ {
+ WabiSabiConfig config = new();
+ Round round = WabiSabiFactory.CreateRound(config);
+ var password = "satoshi";
+
+ var km = ServiceFactory.CreateKeyManager(password);
+ var keyChain = new KeyChain(km, new Kitchen(password));
+ var destinationProvider = new InternalDestinationProvider(km);
+
+ var coins = destinationProvider.GetNextDestinations(2)
+ .Select(dest => (
+ Coin: new Coin(BitcoinFactory.CreateOutPoint(), new TxOut(Money.Coins(1.0m), dest)),
+ OwnershipProof: keyChain.GetOwnershipProof(dest, WabiSabiFactory.CreateCommitmentData(round.Id))))
+ .ToArray();
+
+ Alice alice1 = WabiSabiFactory.CreateAlice(coins[0].Coin, coins[0].OwnershipProof, round: round);
+ round.Alices.Add(alice1);
+
+ Alice alice2 = WabiSabiFactory.CreateAlice(coins[1].Coin, coins[1].OwnershipProof, round: round);
+ round.Alices.Add(alice2);
+
+ using Arena arena = await ArenaBuilder.From(config).CreateAndStartAsync(round);
+
+ var mockRpc = new Mock<IRPCClient>();
+ 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);
+
+ InsecureRandom rnd = InsecureRandom.Instance;
+ var amountClient = new WabiSabiClient(round.AmountCredentialIssuerParameters, rnd, 4300000000000L);
+ var vsizeClient = new WabiSabiClient(round.VsizeCredentialIssuerParameters, rnd, 2000L);
+ var apiClient = new ArenaClient(amountClient, vsizeClient, config.CoordinatorIdentifier, wabiSabiApi);
+
+ round.SetPhase(Phase.TransactionSigning);
+
+ var emptyState = round.Assert<ConstructionState>();
+ var commitmentData = WabiSabiFactory.CreateCommitmentData(round.Id);
+
+ // We can't use ``emptyState.Finalize()` because this is not a valid transaction so we fake it
+ var finalizedEmptyState = new SigningState(round.Parameters, emptyState.Events);
+
+ // No inputs in the coinjoin.
+ await Assert.ThrowsAsync<ArgumentException>(async () =>
+ await apiClient.SignTransactionAsync(round.Id, alice1.Coin, coins[0].OwnershipProof, keyChain, finalizedEmptyState.CreateUnsignedTransaction(), CancellationToken.None));
+
+ var oneInput = emptyState.AddInput(alice1.Coin, alice1.OwnershipProof, commitmentData).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, coins[1].OwnershipProof, keyChain, oneInput.CreateUnsignedTransaction(), CancellationToken.None));
+
+ var twoInputs = emptyState.AddInput(alice1.Coin, alice1.OwnershipProof, commitmentData).AddInput(alice2.Coin, alice2.OwnershipProof, commitmentData).Finalize();
+ round.CoinjoinState = twoInputs;
+
+ Assert.False(round.Assert<SigningState>().IsFullySigned);
+ var unsigned = round.Assert<SigningState>().CreateUnsignedTransaction();
+
+ await apiClient.SignTransactionAsync(round.Id, alice1.Coin, coins[0].OwnershipProof, keyChain, 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, coins[1].OwnershipProof, keyChain, unsigned, CancellationToken.None);
+ Assert.True(round.Assert<SigningState>().IsInputSigned(alice2.Coin.Outpoint));
+
+ Assert.True(round.Assert<SigningState>().IsFullySigned);
+ }
+
+ private async Task TestFullCoinjoinAsync(ScriptPubKeyType scriptPubKeyType, int inputVirtualSize)
{
var config = new WabiSabiConfig { MaxInputCountByRound = 1 };
var round = WabiSabiFactory.CreateRound(WabiSabiFactory.CreateRoundParameters(config));
@@ -43,7 +148,7 @@ public class ArenaClientTests
{
IsCoinBase = false,
Confirmations = 200,
- TxOut = new TxOut(Money.Coins(1m), key.PubKey.WitHash.GetAddress(Network.Main)),
+ TxOut = new TxOut(Money.Coins(1m), key.PubKey.GetAddress(scriptPubKeyType, Network.Main)),
});
mockRpc.Setup(rpc => rpc.EstimateSmartFeeAsync(It.IsAny<int>(), It.IsAny<EstimateSmartFeeMode>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new EstimateSmartFeeResponse
@@ -77,23 +182,22 @@ public class ArenaClientTests
roundState.CreateVsizeCredentialClient(insecureRandom),
config.CoordinatorIdentifier,
wabiSabiApi);
- var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id);
+ var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id, scriptPubKeyType);
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(.75m) - round.Parameters.MiningFeeRate.GetFee(inputVirtualSize) - 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.GetScriptPubKey(ScriptPubKeyType.Segwit).EstimateOutputVsize();
+ var scriptSize = (long)destinationKey1.PubKey.GetScriptPubKey(scriptPubKeyType).EstimateOutputVsize();
- var vsizesToRequest = new[] { round.Parameters.MaxVsizeAllocationPerAlice - (inputVsize + 2 * p2wpkhScriptSize), 2 * p2wpkhScriptSize };
+ var vsizesToRequest = new[] { round.Parameters.MaxVsizeAllocationPerAlice - (inputVirtualSize + 2 * scriptSize), 2 * scriptSize };
// Phase: Input Registration
Assert.Equal(Phase.InputRegistration, round.Phase);
@@ -134,7 +238,7 @@ public class ArenaClientTests
var reissuanceResponse = await bobArenaClient.ReissueCredentialAsync(
round.Id,
amountsToRequest,
- Enumerable.Repeat(p2wpkhScriptSize, 2),
+ Enumerable.Repeat(scriptSize, 2),
connectionConfirmationResponse2.IssuedAmountCredentials.Take(ProtocolConstants.CredentialNumber),
connectionConfirmationResponse2.IssuedVsizeCredentials.Skip(1).Take(ProtocolConstants.CredentialNumber), // first amount is the leftover value
CancellationToken.None);
@@ -151,14 +255,14 @@ public class ArenaClientTests
await bobArenaClient.RegisterOutputAsync(
round.Id,
- destinationKey1.PubKey.GetScriptPubKey(ScriptPubKeyType.Segwit),
+ destinationKey1.PubKey.GetScriptPubKey(scriptPubKeyType),
new[] { amountCred1, zeroAmountCred1 },
new[] { vsizeCred1, zeroVsizeCred1 },
CancellationToken.None);
await bobArenaClient.RegisterOutputAsync(
round.Id,
- destinationKey2.PubKey.GetScriptPubKey(ScriptPubKeyType.Segwit),
+ destinationKey2.PubKey.GetScriptPubKey(scriptPubKeyType),
new[] { amountCred2, zeroAmountCred2 },
new[] { vsizeCred2, zeroVsizeCred2 },
CancellationToken.None);
@@ -172,104 +276,4 @@ public class ArenaClientTests
Assert.Single(tx.Inputs);
Assert.Equal(2 + 1, tx.Outputs.Count); // +1 because it pays coordination fees
}
-
- [Fact]
- public async Task RemoveInputAsyncTestAsync()
- {
- 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(coin, new OwnershipProof(), round, Guid.NewGuid(), false);
- round.Alices.Add(alice);
-
- using Arena arena = await ArenaBuilder.From(config).CreateAndStartAsync(round);
-
- 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 apiClient = new ArenaClient(null!, null!, config.CoordinatorIdentifier, wabiSabiApi);
-
- round.SetPhase(Phase.InputRegistration);
-
- await apiClient.RemoveInputAsync(round.Id, alice.Id, CancellationToken.None);
- Assert.Empty(round.Alices);
- }
-
- [Fact]
- public async Task SignTransactionAsync()
- {
- WabiSabiConfig config = new();
- Round round = WabiSabiFactory.CreateRound(config);
- var password = "satoshi";
-
- var km = ServiceFactory.CreateKeyManager(password);
- var keyChain = new KeyChain(km, new Kitchen(password));
- var destinationProvider = new InternalDestinationProvider(km);
-
- var coins = destinationProvider.GetNextDestinations(2)
- .Select(dest => (
- Coin: new Coin(BitcoinFactory.CreateOutPoint(), new TxOut(Money.Coins(1.0m), dest)),
- OwnershipProof: keyChain.GetOwnershipProof(dest, WabiSabiFactory.CreateCommitmentData(round.Id))))
- .ToArray();
-
- Alice alice1 = WabiSabiFactory.CreateAlice(coins[0].Coin, coins[0].OwnershipProof, round: round);
- round.Alices.Add(alice1);
-
- Alice alice2 = WabiSabiFactory.CreateAlice(coins[1].Coin, coins[1].OwnershipProof, round: round);
- round.Alices.Add(alice2);
-
- using Arena arena = await ArenaBuilder.From(config).CreateAndStartAsync(round);
-
- var mockRpc = new Mock<IRPCClient>();
- 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);
-
- InsecureRandom rnd = InsecureRandom.Instance;
- var amountClient = new WabiSabiClient(round.AmountCredentialIssuerParameters, rnd, 4300000000000L);
- var vsizeClient = new WabiSabiClient(round.VsizeCredentialIssuerParameters, rnd, 2000L);
- var apiClient = new ArenaClient(amountClient, vsizeClient, config.CoordinatorIdentifier, wabiSabiApi);
-
- round.SetPhase(Phase.TransactionSigning);
-
- var emptyState = round.Assert<ConstructionState>();
- var commitmentData = WabiSabiFactory.CreateCommitmentData(round.Id);
-
- // We can't use ``emptyState.Finalize()` because this is not a valid transaction so we fake it
- var finalizedEmptyState = new SigningState(round.Parameters, emptyState.Events);
-
- // No inputs in the coinjoin.
- await Assert.ThrowsAsync<ArgumentException>(async () =>
- await apiClient.SignTransactionAsync(round.Id, alice1.Coin, coins[0].OwnershipProof, keyChain, finalizedEmptyState.CreateUnsignedTransaction(), CancellationToken.None));
-
- var oneInput = emptyState.AddInput(alice1.Coin, alice1.OwnershipProof, commitmentData).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, coins[1].OwnershipProof, keyChain, oneInput.CreateUnsignedTransaction(), CancellationToken.None));
-
- var twoInputs = emptyState.AddInput(alice1.Coin, alice1.OwnershipProof, commitmentData).AddInput(alice2.Coin, alice2.OwnershipProof, commitmentData).Finalize();
- round.CoinjoinState = twoInputs;
-
- Assert.False(round.Assert<SigningState>().IsFullySigned);
- var unsigned = round.Assert<SigningState>().CreateUnsignedTransaction();
-
- await apiClient.SignTransactionAsync(round.Id, alice1.Coin, coins[0].OwnershipProof, keyChain, 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, coins[1].OwnershipProof, keyChain, unsigned, CancellationToken.None);
- Assert.True(round.Assert<SigningState>().IsInputSigned(alice2.Coin.Outpoint));
-
- Assert.True(round.Assert<SigningState>().IsFullySigned);
- }
}

Refactor FullCoinjoinAsyncTestAsync