From 69764515d827afe2f5cd7e62d779ebd3ea4b455e Mon Sep 17 00:00:00 2001 From: nopara73 Date: Wed, 19 Dec 2018 15:38:49 +0100 Subject: [PATCH] Build WasabiSyncer --- WalletWasabi/Helpers/Guard.cs | 17 +- WalletWasabi/Services/IndexDownloader.cs | 8 +- WalletWasabi/Services/WasabiSynchronizer.cs | 472 ++++++++++++++++++++ WalletWasabi/TorSocks5/TorHttpClient.cs | 2 +- 4 files changed, 486 insertions(+), 13 deletions(-) create mode 100644 WalletWasabi/Services/WasabiSynchronizer.cs diff --git a/WalletWasabi/Helpers/Guard.cs b/WalletWasabi/Helpers/Guard.cs index 9136b519aa8..f02cd764e7c 100644 --- a/WalletWasabi/Helpers/Guard.cs +++ b/WalletWasabi/Helpers/Guard.cs @@ -113,38 +113,43 @@ public static string NotNullOrEmptyOrWhitespace(string parameterName, string val } } - public static int MinimumAndNotNull(string parameterName, int? value, int smallest) + public static T MinimumAndNotNull(string parameterName, T value, T smallest) where T : IComparable { NotNull(parameterName, value); - if (value < smallest) + if (value.CompareTo(smallest) < 0) { throw new ArgumentOutOfRangeException(parameterName, value, $"Parameter cannot be less than {smallest}."); } - return (int)value; + return value; } - public static int MaximumAndNotNull(string parameterName, int? value, int greatest) + public static T MaximumAndNotNull(string parameterName, T value, T greatest) where T : IComparable { NotNull(parameterName, value); - if (value > greatest) + if (value.CompareTo(greatest) > 0) { throw new ArgumentOutOfRangeException(parameterName, value, $"Parameter cannot be greater than {greatest}."); } - return (int)value; + return value; } public static T InRangeAndNotNull(string parameterName, T value, T smallest, T greatest) where T : IComparable { NotNull(parameterName, value); + if (value.CompareTo(smallest) < 0) + { throw new ArgumentOutOfRangeException(parameterName, value, $"Parameter cannot be less than {smallest}."); + } if (value.CompareTo(greatest) > 0) + { throw new ArgumentOutOfRangeException(parameterName, value, $"Parameter cannot be greater than {greatest}."); + } return value; } diff --git a/WalletWasabi/Services/IndexDownloader.cs b/WalletWasabi/Services/IndexDownloader.cs index 4f08d0afcbb..7b638335558 100644 --- a/WalletWasabi/Services/IndexDownloader.cs +++ b/WalletWasabi/Services/IndexDownloader.cs @@ -211,6 +211,8 @@ public void Synchronize(TimeSpan requestInterval) { filtersResponse = await WasabiClient.GetFiltersAsync(BestKnownFilter.BlockHash, 1000, Cancel.Token).WithAwaitCancellationAsync(Cancel.Token, 300); // NOT GenSocksServErr + BackendStatus = BackendStatus.Connected; + TorStatus = TorStatus.Running; DoNotGenSocksServFail(); } catch (ConnectionException ex) @@ -218,16 +220,13 @@ public void Synchronize(TimeSpan requestInterval) TorStatus = TorStatus.NotRunning; BackendStatus = BackendStatus.NotConnected; HandleIfGenSocksServFail(ex); - throw; } catch (TorSocks5FailureResponseException ex) { TorStatus = TorStatus.Running; BackendStatus = BackendStatus.NotConnected; - HandleIfGenSocksServFail(ex); - throw; } catch (Exception ex) @@ -235,11 +234,8 @@ public void Synchronize(TimeSpan requestInterval) TorStatus = TorStatus.Running; BackendStatus = BackendStatus.Connected; HandleIfGenSocksServFail(ex); - throw; } - BackendStatus = BackendStatus.Connected; - TorStatus = TorStatus.Running; if (filtersResponse is null) // no-content, we are synced { diff --git a/WalletWasabi/Services/WasabiSynchronizer.cs b/WalletWasabi/Services/WasabiSynchronizer.cs new file mode 100644 index 00000000000..45ce18275f8 --- /dev/null +++ b/WalletWasabi/Services/WasabiSynchronizer.cs @@ -0,0 +1,472 @@ +using NBitcoin; +using NBitcoin.RPC; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using WalletWasabi.Backend.Models; +using WalletWasabi.Backend.Models.Responses; +using WalletWasabi.Exceptions; +using WalletWasabi.Helpers; +using WalletWasabi.Logging; +using WalletWasabi.Models; +using WalletWasabi.WebClients.Wasabi; + +namespace WalletWasabi.Services +{ + public class WasabiSynchronizer : IDisposable, INotifyPropertyChanged + { + #region MembersPropertiesEvents + + public WasabiClient WasabiClient { get; private set; } + + 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 FilterModel _bestKnownFilter; + + public FilterModel BestKnownFilter + { + get => _bestKnownFilter; + + private set + { + if (_bestKnownFilter != value) + { + _bestKnownFilter = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BestKnownFilter))); + } + } + } + + private TorStatus _torStatus; + + public TorStatus TorStatus + { + get => _torStatus; + + private set + { + if (_torStatus != value) + { + _torStatus = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TorStatus))); + } + } + } + + private BackendStatus _backendStatus; + + public BackendStatus BackendStatus + { + get => _backendStatus; + + private set + { + if (_backendStatus != value) + { + _backendStatus = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BackendStatus))); + } + } + } + + public string IndexFilePath { get; private set; } + private ObservableCollection Index { get; set; } + private object IndexLock { get; set; } + + public event PropertyChangedEventHandler PropertyChanged; + + public event EventHandler ResponseArrivedIsGenSocksServFail; + + /// + /// 0: Not started, 1: Running, 2: Stopping, 3: Stopped + /// + private long _running; + + public bool IsRunning => Interlocked.Read(ref _running) == 1; + public bool IsStopping => Interlocked.Read(ref _running) == 2; + + private CancellationTokenSource Cancel { get; set; } + + #endregion MembersPropertiesEvents + + #region ConstructorsAndInitializers + + public WasabiSynchronizer(Network network, string indexFilePath, WasabiClient client) + { + CreateNew(network, indexFilePath, client); + } + + public WasabiSynchronizer(Network network, string indexFilePath, Uri baseUri, IPEndPoint torSocks5EndPoint = null) + { + var client = new WasabiClient(baseUri, torSocks5EndPoint); + CreateNew(network, indexFilePath, client); + } + + private void CreateNew(Network network, string indexFilePath, WasabiClient client) + { + Network = Guard.NotNull(nameof(network), network); + WasabiClient = Guard.NotNull(nameof(client), client); + _running = 0; + Cancel = new CancellationTokenSource(); + BestBlockchainHeight = Height.Unknown; + IndexFilePath = Guard.NotNullOrEmptyOrWhitespace(nameof(indexFilePath), indexFilePath, trim: true); + Index = new ObservableCollection(); + IndexLock = new object(); + + IoHelpers.EnsureContainingDirectoryExists(indexFilePath); + if (File.Exists(IndexFilePath)) + { + if (Network == Network.RegTest) + { + File.Delete(IndexFilePath); // RegTest is not a global ledger, better to delete it. + Index.Add(StartingFilter); + IoHelpers.SafeWriteAllLines(IndexFilePath, Index.Select(x => x.ToLine())); + } + else + { + var height = StartingHeight; + try + { + if (IoHelpers.TryGetSafestFileVersion(IndexFilePath, out var safestFileVerion)) + { + foreach (var line in File.ReadAllLines(safestFileVerion)) + { + var filter = FilterModel.FromLine(line, height); + height++; + Index.Add(filter); + } + } + } + catch (FormatException) + { + // We found a corrupted entry. Stop here. + // Fix the currupted file. + IoHelpers.SafeWriteAllLines(IndexFilePath, Index.Select(x => x.ToLine())); + } + } + } + else + { + Index.Add(StartingFilter); + IoHelpers.SafeWriteAllLines(IndexFilePath, Index.Select(x => x.ToLine())); + } + + BestKnownFilter = Index.Last(); + } + + public void Start(TimeSpan requestInterval, TimeSpan feeQueryRequestInterval, int maxFiltersToSyncAtInitialization) + { + Guard.NotNull(nameof(requestInterval), requestInterval); + Guard.MinimumAndNotNull(nameof(feeQueryRequestInterval), feeQueryRequestInterval, requestInterval); + Guard.MinimumAndNotNull(nameof(maxFiltersToSyncAtInitialization), maxFiltersToSyncAtInitialization, 0); + + Interlocked.Exchange(ref _running, 1); + + Task.Run(async () => + { + try + { + DateTimeOffset lastFeeQueried = DateTimeOffset.UtcNow - feeQueryRequestInterval; + bool ignoreRequestInterval = false; + while (IsRunning) + { + try + { + // If stop was requested return. + if (!IsRunning) + { + return; + } + + EstimateSmartFeeMode? estimateMode = null; + TimeSpan elapsed = DateTimeOffset.UtcNow - lastFeeQueried; + if (elapsed >= feeQueryRequestInterval) + { + estimateMode = EstimateSmartFeeMode.Conservative; + } + + FilterModel startingFilter = BestKnownFilter; + + SynchronizeResponse response; + try + { + response = await WasabiClient.GetSynchronizeAsync(BestKnownFilter.BlockHash, maxFiltersToSyncAtInitialization, estimateMode, Cancel.Token).WithAwaitCancellationAsync(Cancel.Token, 300); + // NOT GenSocksServErr + BackendStatus = BackendStatus.Connected; + TorStatus = TorStatus.Running; + DoNotGenSocksServFail(); + } + catch (ConnectionException ex) + { + TorStatus = TorStatus.NotRunning; + BackendStatus = BackendStatus.NotConnected; + HandleIfGenSocksServFail(ex); + throw; + } + catch (TorSocks5FailureResponseException ex) + { + TorStatus = TorStatus.Running; + BackendStatus = BackendStatus.NotConnected; + HandleIfGenSocksServFail(ex); + throw; + } + catch (Exception ex) + { + TorStatus = TorStatus.Running; + BackendStatus = BackendStatus.Connected; + HandleIfGenSocksServFail(ex); + throw; + } + + if (response.AllFeeEstimate != null && response.AllFeeEstimate.Estimations.Any()) + { + lastFeeQueried = DateTimeOffset.UtcNow; + } + + if (response.Filters.Count() == maxFiltersToSyncAtInitialization) + { + ignoreRequestInterval = true; + } + else + { + ignoreRequestInterval = false; + } + + BestBlockchainHeight = response.BestHeight; + + if (response.FiltersResponseState == FiltersResponseState.NewFilters) + { + List filtersList = response.Filters.ToList(); // performance + + lock (IndexLock) + { + for (int i = 0; i < filtersList.Count; i++) + { + FilterModel filterModel = FilterModel.FromLine(filtersList[i], BestKnownFilter.BlockHeight + 1); + + Index.Add(filterModel); + BestKnownFilter = filterModel; + } + + IoHelpers.SafeWriteAllLines(IndexFilePath, Index.Select(x => x.ToLine())); + var startingFilterHeightPlusOne = startingFilter.BlockHeight + 1; + var bestKnownFilterHeight = BestKnownFilter.BlockHeight; + if (startingFilterHeightPlusOne == bestKnownFilterHeight) + { + Logger.LogInfo($"Downloaded filter for block {startingFilterHeightPlusOne}."); + } + else + { + Logger.LogInfo($"Downloaded filters for blocks from {startingFilterHeightPlusOne} to {bestKnownFilterHeight}."); + } + } + } + else if (response.FiltersResponseState == FiltersResponseState.BestKnownHashNotFound) + { + // Reorg happened + var reorgedHash = BestKnownFilter.BlockHash; + Logger.LogInfo($"REORG Invalid Block: {reorgedHash}"); + // 1. Rollback index + lock (IndexLock) + { + Index.RemoveAt(Index.Count - 1); + BestKnownFilter = Index.Last(); + } + + // 2. Serialize Index. (Remove last line.) + string[] lines = null; + if (IoHelpers.TryGetSafestFileVersion(IndexFilePath, out var safestFileVerion)) + { + lines = File.ReadAllLines(safestFileVerion); + } + IoHelpers.SafeWriteAllLines(IndexFilePath, lines.Take(lines.Length - 1).ToArray()); // It's not async for a reason, I think. + + ignoreRequestInterval = true; + } + else if (response.FiltersResponseState == FiltersResponseState.NoNewFilter) + { + // We are syced. + } + } + catch (Exception ex) + { + if (ex is ConnectionException) + { + Logger.LogError(ex); + try + { + await Task.Delay(3000, Cancel.Token); // Give other threads time to do stuff. + } + catch (TaskCanceledException ex2) + { + Logger.LogTrace(ex2); + } + } + else if (ex is TaskCanceledException || ex is OperationCanceledException || ex is TimeoutException) + { + Logger.LogTrace(ex); + } + else + { + Logger.LogError(ex); + } + } + finally + { + if (IsRunning && !ignoreRequestInterval) + { + try + { + await Task.Delay(requestInterval, Cancel.Token); // Ask for new index in every requestInterval. + } + catch (TaskCanceledException ex) + { + Logger.LogTrace(ex); + } + } + } + } + } + finally + { + if (IsStopping) + { + Interlocked.Exchange(ref _running, 3); + } + } + }); + } + + #endregion ConstructorsAndInitializers + + #region Methods + + public static FilterModel GetStartingFilter(Network network) + { + if (network == Network.Main) + { + return FilterModel.FromLine("0000000000000000001c8018d9cb3b742ef25114f27563e3fc4a1902167f9893:02832810ec08a0", GetStartingHeight(network)); + } + if (network == Network.TestNet) + { + return FilterModel.FromLine("00000000000f0d5edcaeba823db17f366be49a80d91d15b77747c2e017b8c20a:017821b8", GetStartingHeight(network)); + } + if (network == Network.RegTest) + { + return FilterModel.FromLine("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206", GetStartingHeight(network)); + } + throw new NotSupportedException($"{network} is not supported."); + } + + public FilterModel StartingFilter => GetStartingFilter(Network); + + public static Height GetStartingHeight(Network network) => IndexBuilderService.GetStartingHeight(network); + + public Height StartingHeight => GetStartingHeight(Network); + + private void HandleIfGenSocksServFail(Exception ex) + { + // IS GenSocksServFail? + if (ex.ToString().Contains("GeneralSocksServerFailure", StringComparison.OrdinalIgnoreCase)) + { + // IS GenSocksServFail + DoGenSocksServFail(); + } + else + { + // NOT GenSocksServFail + DoNotGenSocksServFail(); + } + } + + private void DoGenSocksServFail() + { + ResponseArrivedIsGenSocksServFail?.Invoke(this, true); + } + + private void DoNotGenSocksServFail() + { + ResponseArrivedIsGenSocksServFail?.Invoke(this, false); + } + + public Height TryGetHeight(uint256 blockHash) + { + lock (IndexLock) + { + return Index.First(x => x.BlockHash == blockHash).BlockHeight; + } + } + + public int GetFiltersLeft() + { + if (BestBlockchainHeight == Height.Unknown || BestBlockchainHeight == Height.MemPool || BestKnownFilter.BlockHeight == Height.Unknown || BestKnownFilter.BlockHeight == Height.MemPool) + { + return -1; + } + return BestBlockchainHeight.Value - BestKnownFilter.BlockHeight.Value; + } + + #endregion Methods + + #region IDisposable Support + + private volatile bool _disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + if (IsRunning) + { + Interlocked.Exchange(ref _running, 2); + } + Cancel?.Cancel(); + while (IsStopping) + { + Task.Delay(50).GetAwaiter().GetResult(); // DO NOT MAKE IT ASYNC (.NET Core threading brainfart) + } + + Cancel?.Dispose(); + WasabiClient?.Dispose(); + } + + _disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // GC.SuppressFinalize(this); + } + + #endregion IDisposable Support + } +} diff --git a/WalletWasabi/TorSocks5/TorHttpClient.cs b/WalletWasabi/TorSocks5/TorHttpClient.cs index b539c7de043..859155b3234 100644 --- a/WalletWasabi/TorSocks5/TorHttpClient.cs +++ b/WalletWasabi/TorSocks5/TorHttpClient.cs @@ -47,7 +47,7 @@ private set public TorSocks5Client TorSocks5Client { get; private set; } - private static AsyncLock AsyncLock { get; } = new AsyncLock(); // We make everything synchronous, so slow, but at least stable + private static AsyncLock AsyncLock { get; } = new AsyncLock(); // We make everything synchronous, so slow, but at least stable. public TorHttpClient(Uri baseUri, IPEndPoint torSocks5EndPoint, bool isolateStream = false) {