Add merger tests
MODIFY
WalletWasabi.Tests/Global.cs
WalletWasabi.Tests/Global.cs
+using NBitcoin;
using System;
using System.Collections.Generic;
using System.IO;
@@ -5,6 +6,7 @@ using System.Net;
using System.Text;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
+using WalletWasabi.Models;
namespace WalletWasabi.Tests
{
@@ -29,5 +31,12 @@ namespace WalletWasabi.Tests
Logger.SetMinimumLevel(LogLevel.Info);
Logger.SetModes(LogMode.Debug, LogMode.Console, LogMode.File);
}
+
+ public static SmartTransaction GenerateRandomSmartTransaction()
+ {
+ var tx = Transaction.Create(Network.Main);
+ tx.Outputs.Add(Money.Coins(1), new Key());
+ return new SmartTransaction(tx, Height.Mempool);
+ }
}
}

ADD
WalletWasabi.Tests/UnitTests/Transactions/OperationMergerTests.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using WalletWasabi.Transactions;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.Transactions
+{
+ public class OperationMergerTests
+ {
+ [Fact]
+ public void CanMergeSingleOperations()
+ {
+ var tx = Global.GenerateRandomSmartTransaction();
+
+ var append = new Append(tx);
+ var unmergedAppendOperations = new[] { append };
+ var mergedOperations = OperationMerger.Merge(unmergedAppendOperations);
+ var expectedOperation = Assert.Single(mergedOperations);
+ var expectedAppendOperationInstance = Assert.IsType<Append>(expectedOperation);
+ var expectedTx = Assert.Single(expectedAppendOperationInstance.Transactions);
+ Assert.Equal(expectedTx, tx);
+
+ var remove = new Remove(tx.GetHash());
+ var unmergedRemoveOperations = new[] { remove };
+ mergedOperations = OperationMerger.Merge(unmergedRemoveOperations);
+ expectedOperation = Assert.Single(mergedOperations);
+ var expectedRemoveOperationInstance = Assert.IsType<Remove>(expectedOperation);
+ var expectedTxHash = Assert.Single(expectedRemoveOperationInstance.Transactions);
+ Assert.Equal(expectedTxHash, tx.GetHash());
+ }
+
+ [Fact]
+ public void CanMergeComplexOperations()
+ {
+ var tx1 = Global.GenerateRandomSmartTransaction();
+ var tx2 = Global.GenerateRandomSmartTransaction();
+ var tx3 = Global.GenerateRandomSmartTransaction();
+ var tx4 = Global.GenerateRandomSmartTransaction();
+ var tx5 = Global.GenerateRandomSmartTransaction();
+ var tx6 = Global.GenerateRandomSmartTransaction();
+
+ var a1 = new Append(tx1, tx2);
+ var a2 = new Append(tx3, tx4, tx5);
+ var r1 = new Remove(tx6.GetHash());
+ var a3 = new Append(tx6);
+ var r2 = new Remove(tx1.GetHash(), tx2.GetHash());
+ var r3 = new Remove(tx6.GetHash());
+ var a4 = new Append(tx1);
+
+ var unmergedOperations = new ITxStoreOperation[] { a1, a2, a3, a4, r1, r2, r3 };
+ var mergedOperations = OperationMerger.Merge(unmergedOperations);
+ Assert.Equal(2, mergedOperations.Count());
+
+ unmergedOperations = new ITxStoreOperation[] { r1, r2, r3, a1, a2, a3, a4 };
+ mergedOperations = OperationMerger.Merge(unmergedOperations);
+ Assert.Equal(2, mergedOperations.Count());
+
+ unmergedOperations = new ITxStoreOperation[] { a1, r1, a2, r2, a3, r3, a4 };
+ mergedOperations = OperationMerger.Merge(unmergedOperations);
+ Assert.Equal(7, mergedOperations.Count());
+
+ unmergedOperations = new ITxStoreOperation[] { a1, a2, r1, a3, r2, r3, a4 };
+ mergedOperations = OperationMerger.Merge(unmergedOperations);
+ Assert.Equal(5, mergedOperations.Count());
+ var mergedOperationsArray = mergedOperations.ToArray();
+ Assert.Equal(5, Assert.IsType<Append>(mergedOperationsArray[0]).Transactions.Count());
+ Assert.Single(Assert.IsType<Remove>(mergedOperationsArray[1]).Transactions);
+ Assert.Single(Assert.IsType<Append>(mergedOperationsArray[2]).Transactions);
+ Assert.Equal(3, Assert.IsType<Remove>(mergedOperationsArray[3]).Transactions.Count());
+ Assert.Single(Assert.IsType<Append>(mergedOperationsArray[4]).Transactions);
+ }
+ }
+}

MODIFY
WalletWasabi.Tests/UnitTests/Transactions/TransactionStoreTests.cs
WalletWasabi.Tests/UnitTests/Transactions/TransactionStoreTests.cs
@@ -2,9 +2,11 @@ using NBitcoin;
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WalletWasabi.Models;
+using WalletWasabi.Transactions;
using Xunit;
namespace WalletWasabi.Tests.UnitTests.Transactions
@@ -37,8 +39,7 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
Assert.True(txStore.IsEmpty());
- var tx = Transaction.Create(network);
- var stx = new SmartTransaction(tx, Height.Mempool, firstSeen: DateTimeOffset.UtcNow);
+ var stx = Global.GenerateRandomSmartTransaction();
var isAdded = txStore.TryAdd(stx);
Assert.True(isAdded);
var isRemoved = txStore.TryRemove(stx.GetHash(), out SmartTransaction removed);
@@ -50,11 +51,11 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
Assert.False(isAdded);
Assert.False(txStore.TryAdd(stx));
Assert.False(txStore.IsEmpty());
- Assert.True(txStore.TryGetTransaction(tx.GetHash(), out SmartTransaction sameStx));
- Assert.True(txStore.Contains(tx.GetHash()));
+ Assert.True(txStore.TryGetTransaction(stx.GetHash(), out SmartTransaction sameStx));
+ Assert.True(txStore.Contains(stx.GetHash()));
Assert.Equal(stx, sameStx);
- txStore.TryRemove(tx.GetHash(), out _);
+ txStore.TryRemove(stx.GetHash(), out _);
Assert.True(txStore.IsEmpty());
Assert.Empty(txStore.GetTransactions());
txStore.TryAdd(stx);

MODIFY
WalletWasabi/Transactions/Append.cs
WalletWasabi/Transactions/Append.cs
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Text;
using WalletWasabi.Models;
@@ -9,6 +10,12 @@ namespace WalletWasabi.Transactions
{
public IEnumerable<SmartTransaction> Transactions { get; }
+ public bool IsEmpty => Transactions is null || !Transactions.Any();
+
+ public Append(params SmartTransaction[] transactions) : this(transactions as IEnumerable<SmartTransaction>)
+ {
+ }
+
public Append(IEnumerable<SmartTransaction> transactions)
{
Transactions = transactions;

MODIFY
WalletWasabi/Transactions/ITxStoreOperation.cs
WalletWasabi/Transactions/ITxStoreOperation.cs
@@ -6,5 +6,6 @@ namespace WalletWasabi.Transactions
{
public interface ITxStoreOperation
{
+ public bool IsEmpty { get; }
}
}

ADD
WalletWasabi/Transactions/OperationMerger.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using WalletWasabi.Models;
+
+namespace WalletWasabi.Transactions
+{
+ public static class OperationMerger
+ {
+ /// <summary>
+ /// Merges the operations in a way that it leaves the order, but two consequtive remove or append cannot follow each other.
+ /// </summary>
+ public static IEnumerable<ITxStoreOperation> Merge(IEnumerable<ITxStoreOperation> operations)
+ {
+ var tempToAppends = new List<SmartTransaction>();
+ var tempToRemoves = new List<uint256>();
+ bool wasLastOpAppend = operations.First() is Append;
+ foreach (ITxStoreOperation op in operations)
+ {
+ if (wasLastOpAppend)
+ {
+ if (op is Append opApp)
+ {
+ tempToAppends.AddRange(opApp.Transactions);
+ }
+ else if (op is Remove opRem)
+ {
+ yield return new Append(tempToAppends);
+ tempToAppends = new List<SmartTransaction>();
+ tempToRemoves.AddRange(opRem.Transactions);
+ }
+ }
+ else
+ {
+ if (op is Remove opRem)
+ {
+ tempToRemoves.AddRange(opRem.Transactions);
+ }
+ else if (op is Append opApp)
+ {
+ yield return new Remove(tempToRemoves);
+ tempToRemoves = new List<uint256>();
+ tempToAppends.AddRange(opApp.Transactions);
+ }
+ }
+
+ wasLastOpAppend = op is Append;
+ }
+
+ if (tempToAppends.Any())
+ {
+ yield return new Append(tempToAppends);
+ }
+
+ if (tempToRemoves.Any())
+ {
+ yield return new Remove(tempToRemoves);
+ }
+ }
+ }
+}

MODIFY
WalletWasabi/Transactions/Remove.cs
WalletWasabi/Transactions/Remove.cs
using NBitcoin;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Text;
namespace WalletWasabi.Transactions
@@ -8,6 +9,11 @@ namespace WalletWasabi.Transactions
public class Remove : ITxStoreOperation
{
public IEnumerable<uint256> Transactions { get; }
+ public bool IsEmpty => Transactions is null || !Transactions.Any();
+
+ public Remove(params uint256[] transactions) : this(transactions as IEnumerable<uint256>)
+ {
+ }
public Remove(IEnumerable<uint256> transactions)
{

MODIFY
WalletWasabi/Transactions/TransactionStore.cs
WalletWasabi/Transactions/TransactionStore.cs
@@ -243,20 +243,57 @@ namespace WalletWasabi.Transactions
private async Task TryRemoveFromFileAsync(IEnumerable<uint256> transactionIds)
=> await TryCommitToFileAsync(new Remove(transactionIds)).ConfigureAwait(false);
+ private List<ITxStoreOperation> Operations { get; } = new List<ITxStoreOperation>();
+ private object OperationsLock { get; } = new object();
+
private async Task TryCommitToFileAsync(ITxStoreOperation operation)
{
try
{
- if (operation is Append appendOperation)
+ if (operation is null || operation.IsEmpty)
{
- IEnumerable<SmartTransaction> toAppends = appendOperation.Transactions;
- if (toAppends is null || !toAppends.Any())
+ return;
+ }
+
+ // Make sure that only one call can continue.
+ lock (OperationsLock)
+ {
+ var isRunning = Operations.Any();
+ Operations.Add(operation);
+ if (isRunning)
{
return;
}
+ }
+
+ // Wait until the operation list calms down.
+ List<ITxStoreOperation> operationsToExecute;
+ while (true)
+ {
+ var count = Operations.Count;
+
+ await Task.Delay(100).ConfigureAwait(false);
+
+ lock (OperationsLock)
+ {
+ if (count == Operations.Count)
+ {
+ // Merge operations.
+ operationsToExecute = OperationMerger.Merge(Operations).ToList();
+ Operations.Clear();
+ break;
+ }
+ }
+ }
using (await TransactionsFileManager.Mutex.LockAsync().ConfigureAwait(false))
{
+ foreach (ITxStoreOperation op in operationsToExecute)
+ {
+ if (op is Append appendOperation)
+ {
+ var toAppends = appendOperation.Transactions;
+
try
{
await TransactionsFileManager.AppendAllLinesAsync(toAppends.ToBlockchainOrderedLines()).ConfigureAwait(false);
@@ -266,17 +303,10 @@ namespace WalletWasabi.Transactions
await SerializeAllTransactionsNoMutexAsync().ConfigureAwait(false);
}
}
- }
- else if (operation is Remove removeOperation)
- {
- IEnumerable<uint256> toRemoves = removeOperation.Transactions;
- if (toRemoves is null || !toRemoves.Any())
+ else if (op is Remove removeOperation)
{
- return;
- }
+ var toRemoves = removeOperation.Transactions;
- using (await TransactionsFileManager.Mutex.LockAsync().ConfigureAwait(false))
- {
string[] allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false);
var toSerialize = new List<string>();
foreach (var line in allLines)
@@ -302,10 +332,11 @@ namespace WalletWasabi.Transactions
await SerializeAllTransactionsNoMutexAsync().ConfigureAwait(false);
}
}
- }
else
{
- throw new ArgumentException(nameof(operation));
+ throw new NotSupportedException();
+ }
+ }
}
}
catch (Exception ex)

Add merger tests