Add HashChain
MODIFY
WalletWasabi.Gui/ViewModels/StatusBarViewModel.cs
WalletWasabi.Gui/ViewModels/StatusBarViewModel.cs
@@ -18,6 +18,7 @@ using WalletWasabi.Gui.Tabs;
using WalletWasabi.Helpers;
using WalletWasabi.Models;
using WalletWasabi.Services;
+using WalletWasabi.Stores;
namespace WalletWasabi.Gui.ViewModels
{
@@ -33,6 +34,7 @@ namespace WalletWasabi.Gui.ViewModels
private CompositeDisposable Disposables { get; } = new CompositeDisposable();
private NodesCollection Nodes { get; }
private WasabiSynchronizer Synchronizer { get; }
+ private HashChain HashChain { get; }
private bool UseTor { get; }
@@ -52,6 +54,7 @@ namespace WalletWasabi.Gui.ViewModels
UpdateStatus = UpdateStatus.Latest;
Nodes = nodes;
Synchronizer = synchronizer;
+ HashChain = synchronizer.BitcoinStore.HashChain;
BlocksLeft = 0;
FiltersLeft = 0;
UseTor = Global.Config.UseTor.Value; // Don't make it dynamic, because if you change this config settings only next time will it activate.
@@ -76,11 +79,6 @@ namespace WalletWasabi.Gui.ViewModels
BlocksLeft = x.EventArgs;
}).DisposeWith(Disposables);
- Observable.FromEventPattern(synchronizer.BitcoinStore.IndexStore, nameof(synchronizer.BitcoinStore.IndexStore.NewFilter)).Subscribe(async x =>
- {
- FiltersLeft = await Synchronizer.GetFiltersLeftAsync();
- }).DisposeWith(Disposables);
-
synchronizer.WhenAnyValue(x => x.TorStatus).Subscribe(status =>
{
SetTor(status);
@@ -92,9 +90,9 @@ namespace WalletWasabi.Gui.ViewModels
Backend = Synchronizer.BackendStatus;
}).DisposeWith(Disposables);
- synchronizer.WhenAnyValue(x => x.BestBlockchainHeight).Subscribe(async _ =>
+ HashChain.WhenAnyValue(x => x.HashesLeft).Subscribe(x =>
{
- FiltersLeft = await Synchronizer.GetFiltersLeftAsync();
+ FiltersLeft = x;
}).DisposeWith(Disposables);
synchronizer.WhenAnyValue(x => x.UsdExchangeRate).Subscribe(usd =>

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -545,7 +545,7 @@ namespace WalletWasabi.Tests
Assert.Single(wallet.Coins);
var firstCoin = wallet.Coins.Single();
Assert.Equal(Money.Coins(0.1m), firstCoin.Amount);
- Assert.Equal((await bitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHeight, firstCoin.Height);
+ Assert.Equal(new Height(bitcoinStore.HashChain.TipHeight), firstCoin.Height);
Assert.InRange(firstCoin.Index, 0U, 1U);
Assert.False(firstCoin.Unavailable);
Assert.Equal("foo label", firstCoin.Label);
@@ -574,9 +574,9 @@ namespace WalletWasabi.Tests
var thirdCoin = wallet.Coins.OrderBy(x => x.Height).Last();
Assert.Equal(Money.Coins(0.01m), secondCoin.Amount);
Assert.Equal(Money.Coins(0.02m), thirdCoin.Amount);
- Assert.Equal((await bitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHeight.Value - 2, firstCoin.Height.Value);
- Assert.Equal((await bitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHeight.Value - 1, secondCoin.Height.Value);
- Assert.Equal((await bitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHeight, thirdCoin.Height);
+ Assert.Equal(new Height(bitcoinStore.HashChain.TipHeight).Value - 2, firstCoin.Height.Value);
+ Assert.Equal(new Height(bitcoinStore.HashChain.TipHeight).Value - 1, secondCoin.Height.Value);
+ Assert.Equal(new Height(bitcoinStore.HashChain.TipHeight), thirdCoin.Height);
Assert.False(thirdCoin.Unavailable);
Assert.Equal("foo label", firstCoin.Label);
Assert.Equal("bar label", secondCoin.Label);
@@ -633,7 +633,7 @@ namespace WalletWasabi.Tests
var rbfCoin = wallet.Coins.Where(x => x.TransactionId == tx4bumpRes.TransactionId).Single();
Assert.Equal(Money.Coins(0.03m), rbfCoin.Amount);
- Assert.Equal((await bitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHeight.Value - 2, rbfCoin.Height.Value);
+ Assert.Equal(new Height(bitcoinStore.HashChain.TipHeight).Value - 2, rbfCoin.Height.Value);
Assert.False(rbfCoin.Unavailable);
Assert.Equal("bar label", rbfCoin.Label);
Assert.Equal(key2.P2wpkhScript, rbfCoin.ScriptPubKey);
@@ -667,7 +667,7 @@ namespace WalletWasabi.Tests
await rpc.GenerateAsync(1);
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 1);
var res = await rpc.GetTxOutAsync(mempoolCoin.TransactionId, (int)mempoolCoin.Index, true);
- Assert.Equal((await bitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHeight, mempoolCoin.Height);
+ Assert.Equal(new Height(bitcoinStore.HashChain.TipHeight), mempoolCoin.Height);
}
finally
{
@@ -1084,7 +1084,7 @@ namespace WalletWasabi.Tests
await rpc.GenerateAsync(1);
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 1);
- var bestHeight = (await bitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHeight;
+ var bestHeight = new Height(bitcoinStore.HashChain.TipHeight);
Assert.Contains($"{Constants.ChangeOfSpecialLabelStart}outgoing, outgoing2{Constants.ChangeOfSpecialLabelEnd}", wallet.Coins.Where(x => x.Height == bestHeight).Select(x => x.Label));
Assert.Contains($"{Constants.ChangeOfSpecialLabelStart}outgoing, outgoing2{Constants.ChangeOfSpecialLabelEnd}", keyManager.GetKeys().Select(x => x.Label));

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -235,12 +235,12 @@ namespace WalletWasabi.Services
do
{
await Task.Delay(100);
- if (Synchronizer is null)
+ if (Synchronizer is null || BitcoinStore?.HashChain is null)
{
return;
}
// Make sure fully synced and this filter is the lastest filter.
- if (await Synchronizer.GetFiltersLeftAsync() != 0 || (await BitcoinStore.IndexStore.GetBestKnownFilterAsync()).BlockHash != filterModel.BlockHash)
+ if (BitcoinStore.HashChain.HashesLeft != 0 || BitcoinStore.HashChain.TipHash != filterModel.BlockHash)
{
return;
}

MODIFY
WalletWasabi/Services/WasabiSynchronizer.cs
WalletWasabi/Services/WasabiSynchronizer.cs
@@ -32,22 +32,6 @@ namespace WalletWasabi.Services
public Network Network { get; private set; }
- private Height _bestBlockchainHeight;
-
- public Height BestBlockchainHeight
- {
- get => _bestBlockchainHeight;
-
- private set
- {
- if (_bestBlockchainHeight != value)
- {
- _bestBlockchainHeight = value;
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BestBlockchainHeight)));
- }
- }
- }
-
private decimal _usdExchangeRate;
/// <summary>
@@ -170,7 +154,6 @@ namespace WalletWasabi.Services
LastResponse = null;
_running = 0;
Cancel = new CancellationTokenSource();
- BestBlockchainHeight = Height.Unknown;
BitcoinStore = Guard.NotNull(nameof(bitcoinStore), bitcoinStore);
}
@@ -215,8 +198,7 @@ namespace WalletWasabi.Services
return;
}
- var bestKnownFilter = await BitcoinStore.IndexStore.GetBestKnownFilterAsync();
- response = await WasabiClient.GetSynchronizeAsync(bestKnownFilter.BlockHash, maxFiltersToSyncAtInitialization, estimateMode, Cancel.Token).WithAwaitCancellationAsync(Cancel.Token, 300);
+ response = await WasabiClient.GetSynchronizeAsync(BitcoinStore.HashChain.TipHash, maxFiltersToSyncAtInitialization, estimateMode, Cancel.Token).WithAwaitCancellationAsync(Cancel.Token, 300);
// NOT GenSocksServErr
BackendStatus = BackendStatus.Connected;
TorStatus = TorStatus.Running;
@@ -259,7 +241,7 @@ namespace WalletWasabi.Services
ignoreRequestInterval = false;
}
- BestBlockchainHeight = response.BestHeight;
+ BitcoinStore.HashChain.UpdateServerTipHeight(response.BestHeight);
ExchangeRate exchangeRate = response.ExchangeRates.FirstOrDefault();
if (exchangeRate != default && exchangeRate.Rate != 0)
{
@@ -399,16 +381,6 @@ namespace WalletWasabi.Services
ResponseArrivedIsGenSocksServFail?.Invoke(this, false);
}
- public async Task<int> GetFiltersLeftAsync()
- {
- var bestKnownFilter = await BitcoinStore.IndexStore.GetBestKnownFilterAsync();
- if (BestBlockchainHeight == Height.Unknown || BestBlockchainHeight == Height.MemPool || bestKnownFilter.BlockHeight == Height.Unknown || bestKnownFilter.BlockHeight == Height.MemPool)
- {
- return -1;
- }
- return BestBlockchainHeight.Value - bestKnownFilter.BlockHeight.Value;
- }
-
public Money GetFeeRate(int feeTarget)
{
return AllFeeEstimate.GetFeeRate(feeTarget);

MODIFY
WalletWasabi/Stores/BitcoinStore.cs
WalletWasabi/Stores/BitcoinStore.cs
@@ -18,6 +18,7 @@ namespace WalletWasabi.Stores
private Network Network { get; set; }
public IndexStore IndexStore { get; private set; }
+ public HashChain HashChain { get; private set; }
public async Task InitializeAsync(string workFolderPath, Network network)
{
@@ -29,6 +30,7 @@ namespace WalletWasabi.Stores
IndexStore = new IndexStore();
var indexStoreFolderPath = Path.Combine(WorkFolderPath, Network.ToString());
await IndexStore.InitializeAsync(indexStoreFolderPath, Network);
+ HashChain = IndexStore.HashChain;
}
}
}

ADD
WalletWasabi/Stores/HashChain.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using WalletWasabi.Helpers;
+using WalletWasabi.Models;
+
+namespace WalletWasabi.Stores
+{
+ /// <summary>
+ /// High performance chain index and cache.
+ /// </summary>
+ public class HashChain : INotifyPropertyChanged
+ {
+ private int _tipHeight;
+ private uint256 _tipHash;
+ private int _serverTipHeight;
+ private int _hashesLeft;
+
+ private SortedDictionary<int, uint256> Chain { get; }
+ private object Lock { get; }
+
+ public int TipHeight
+ {
+ get => _tipHeight;
+ private set
+ {
+ if (_tipHeight != value)
+ {
+ _tipHeight = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TipHeight)));
+ }
+ }
+ }
+
+ public uint256 TipHash
+ {
+ get => _tipHash;
+ private set
+ {
+ if (_tipHash != value)
+ {
+ _tipHash = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TipHash)));
+ }
+ }
+ }
+
+ public int ServerTipHeight
+ {
+ get => _serverTipHeight;
+ private set
+ {
+ if (_serverTipHeight != value)
+ {
+ _serverTipHeight = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ServerTipHeight)));
+ }
+ }
+ }
+
+ public int HashesLeft
+ {
+ get => _hashesLeft;
+ private set
+ {
+ if (_hashesLeft != value)
+ {
+ _hashesLeft = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HashesLeft)));
+ }
+ }
+ }
+
+ public HashChain()
+ {
+ Chain = new SortedDictionary<int, uint256>();
+ Lock = new object();
+ ServerTipHeight = 0;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void AddOrReplace(int height, uint256 hash)
+ {
+ lock (Lock)
+ {
+ Chain.AddOrReplace(height, hash);
+ TipHeight = height;
+ TipHash = hash;
+ SetHashesLeft();
+ }
+ }
+
+ private void SetHashesLeft()
+ {
+ HashesLeft = Math.Max(0, ServerTipHeight - TipHeight);
+ }
+
+ public void RemoveLast()
+ {
+ lock (Lock)
+ {
+ if (Chain.Any())
+ {
+ Chain.Remove(Chain.Keys.Max());
+ var last = Chain.Last();
+ TipHeight = last.Key;
+ TipHash = last.Value;
+ SetHashesLeft();
+ }
+ }
+ }
+
+ public void UpdateServerTipHeight(int height)
+ {
+ lock (Lock)
+ {
+ ServerTipHeight = height;
+ HashesLeft = ServerTipHeight - TipHeight;
+ }
+ }
+ }
+}

MODIFY
WalletWasabi/Stores/IndexStore.cs
WalletWasabi/Stores/IndexStore.cs
@@ -24,6 +24,7 @@ namespace WalletWasabi.Stores
private string WorkFolderPath { get; set; }
private Network Network { get; set; }
private IoManager IndexFileManager { get; set; }
+ public HashChain HashChain { get; private set; }
private FilterModel StartingFilter { get; set; }
private Height StartingHeight { get; set; }
@@ -46,6 +47,7 @@ namespace WalletWasabi.Stores
StartingHeight = StartingFilters.GetStartingHeight(Network);
Index = new List<FilterModel>();
+ HashChain = new HashChain();
IndexLock = new AsyncLock();
@@ -75,6 +77,7 @@ namespace WalletWasabi.Stores
var filter = FilterModel.FromHeightlessLine(line, height);
height++;
Index.Add(filter);
+ HashChain.AddOrReplace(filter.BlockHeight.Value, filter.BlockHash);
}
}
catch (FormatException)
@@ -108,6 +111,7 @@ namespace WalletWasabi.Stores
using (await IndexLock.LockAsync())
{
Index.Add(filter);
+ HashChain.AddOrReplace(filter.BlockHeight.Value, filter.BlockHash);
}
NewFilter?.Invoke(this, filter); // Event always outside the lock.
@@ -122,6 +126,7 @@ namespace WalletWasabi.Stores
{
filter = Index.Last();
Index.RemoveLast();
+ HashChain.RemoveLast();
}
Reorged?.Invoke(this, filter);
@@ -178,17 +183,6 @@ namespace WalletWasabi.Stores
}
}
- public async Task<FilterModel> GetBestKnownFilterAsync()
- {
- FilterModel ret = null;
-
- using (await IndexLock.LockAsync())
- {
- ret = Index.Last();
- }
- return ret;
- }
-
public async Task<Height?> TryGetHeightAsync(uint256 blockHash)
{
Height? ret = null;

Add HashChain