Create HybridBestHeightProvider
MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -17,6 +17,7 @@ using WalletWasabi.BitcoinCore.Monitoring;
using WalletWasabi.Blockchain.Analysis.FeesEstimation;
using WalletWasabi.Blockchain.Blocks;
using WalletWasabi.Blockchain.Mempool;
+using WalletWasabi.Blockchain.Statistics;
using WalletWasabi.Blockchain.TransactionBroadcasting;
using WalletWasabi.Blockchain.TransactionProcessing;
using WalletWasabi.Blockchain.Transactions;
@@ -56,6 +57,7 @@ namespace WalletWasabi.Gui
public NodesGroup Nodes { get; private set; }
public WasabiSynchronizer Synchronizer { get; private set; }
public HybridFeeProvider FeeProvider { get; private set; }
+ public HybridBestHeightProvider BestHeightProvider { get; private set; }
public WalletManager WalletManager { get; }
public TransactionBroadcaster TransactionBroadcaster { get; set; }
public CoinJoinProcessor CoinJoinProcessor { get; set; }
@@ -215,7 +217,7 @@ namespace WalletWasabi.Gui
HostedServices.Register<BlockNotifier>(new BlockNotifier(TimeSpan.FromSeconds(7), BitcoinCoreNode.RpcClient, BitcoinCoreNode.P2pNode), "Block Notifier");
HostedServices.Register<RpcMonitor>(new RpcMonitor(TimeSpan.FromSeconds(7), BitcoinCoreNode.RpcClient), "RPC Monitor");
- HostedServices.Register<RpcFeeProvider>(new RpcFeeProvider(TimeSpan.FromMinutes(1), BitcoinCoreNode.RpcClient), "RPC Fee Provider");
+ HostedServices.Register<RpcFeeProvider>(new RpcFeeProvider(TimeSpan.FromMinutes(1), BitcoinCoreNode.RpcClient, HostedServices.Get<RpcMonitor>()), "RPC Fee Provider");
}
}
catch (Exception ex)
@@ -231,6 +233,12 @@ namespace WalletWasabi.Gui
#endregion FeeProviderInitialization
+ #region BestHeightProviderInitialization
+
+ BestHeightProvider = new HybridBestHeightProvider(Synchronizer, HostedServices.GetOrDefault<RpcMonitor>());
+
+ #endregion BestHeightProviderInitialization
+
await HostedServices.StartAllAsync(cancel).ConfigureAwait(false);
cancel.ThrowIfCancellationRequested();

MODIFY
WalletWasabi/BitcoinCore/Monitoring/RpcFeeProvider.cs
WalletWasabi/BitcoinCore/Monitoring/RpcFeeProvider.cs
@@ -12,14 +12,16 @@ namespace WalletWasabi.BitcoinCore.Monitoring
{
public class RpcFeeProvider : PeriodicRunner
{
- public RpcFeeProvider(TimeSpan period, IRPCClient rpcClient) : base(period)
+ public RpcFeeProvider(TimeSpan period, IRPCClient rpcClient, RpcMonitor rpcMonitor) : base(period)
{
RpcClient = rpcClient;
+ RpcMonitor = rpcMonitor;
}
public event EventHandler<AllFeeEstimate>? AllFeeEstimateArrived;
public IRPCClient RpcClient { get; set; }
+ public RpcMonitor RpcMonitor { get; }
public AllFeeEstimate? LastAllFeeEstimate { get; private set; }
public bool InError { get; private set; } = false;
@@ -28,17 +30,16 @@ namespace WalletWasabi.BitcoinCore.Monitoring
try
{
var allFeeEstimate = await RpcClient.EstimateAllFeeAsync(EstimateSmartFeeMode.Conservative, true).ConfigureAwait(false);
- var uptime = await RpcClient.UptimeAsync().ConfigureAwait(false);
- // If Core was running for a day already, then we can be pretty sure that the estimate is accurate.
+ // If Core was running for a day already && it's synchronized, then we can be pretty sure that the estimate is accurate.
// It could also be accurate if Core was only shut down for a few minutes, but that's hard to figure out.
- allFeeEstimate.IsAccurate = uptime > TimeSpan.FromDays(1);
+ allFeeEstimate.IsAccurate = RpcMonitor.RpcStatus.Synchronized && await RpcClient.UptimeAsync().ConfigureAwait(false) > TimeSpan.FromDays(1);
+ LastAllFeeEstimate = allFeeEstimate;
if (allFeeEstimate?.Estimations?.Any() is true)
{
AllFeeEstimateArrived?.Invoke(this, allFeeEstimate);
}
- LastAllFeeEstimate = allFeeEstimate;
InError = false;
}
catch

MODIFY
WalletWasabi/BitcoinCore/Monitoring/RpcMonitor.cs
WalletWasabi/BitcoinCore/Monitoring/RpcMonitor.cs
@@ -14,8 +14,8 @@ namespace WalletWasabi.BitcoinCore.Monitoring
public RpcMonitor(TimeSpan period, IRPCClient rpcClient) : base(period)
{
- RpcStatus = RpcStatus.Unresponsive;
- RpcClient = Guard.NotNull(nameof(rpcClient), rpcClient);
+ _rpcStatus = RpcStatus.Unresponsive;
+ RpcClient = rpcClient;
}
public event EventHandler<RpcStatus>? RpcStatusChanged;

MODIFY
WalletWasabi/Blockchain/Analysis/FeesEstimation/HybridFeeProvider.cs
WalletWasabi/Blockchain/Analysis/FeesEstimation/HybridFeeProvider.cs
@@ -4,6 +4,7 @@ using System.Linq;
using WalletWasabi.Backend.Models.Responses;
using WalletWasabi.BitcoinCore.Monitoring;
using WalletWasabi.Logging;
+using WalletWasabi.Models;
using WalletWasabi.Services;
namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
@@ -28,13 +29,8 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
RpcFeeProvider.AllFeeEstimateArrived += OnAllFeeEstimateArrived;
}
- var syncerEstimate = Synchronizer.LastResponse?.AllFeeEstimate;
- var rpcEstimate = RpcFeeProvider?.LastAllFeeEstimate;
- var betterEstimate = rpcEstimate?.IsAccurate is true ? rpcEstimate : syncerEstimate;
- if (betterEstimate is not null)
- {
- SetAllFeeEstimate(betterEstimate);
- }
+ SetAllFeeEstimateIfLooksBetter(RpcFeeProvider?.LastAllFeeEstimate);
+ SetAllFeeEstimateIfLooksBetter(Synchronizer.LastResponse?.AllFeeEstimate);
}
public event EventHandler<AllFeeEstimate>? AllFeeEstimateChanged;
@@ -62,34 +58,59 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
{
if (AllFeeEstimate is null)
{
- // If the fee was never set yet, then we should set it regardless where it came from.
+ // If it wasn't set before, then set it regardless everything.
notify = SetAllFeeEstimate(fees);
}
- else if (!AllFeeEstimate.IsAccurate && fees.IsAccurate)
+ else if (sender is WasabiSynchronizer syncer)
{
- // If the fee was inaccurate and the new fee is accurate, then we should set it regardless where it came from.
+ if (RpcFeeProvider is null)
+ {
+ // If user doesn't use full node, then set it, this is the best we got.
notify = SetAllFeeEstimate(fees);
}
- else if (AllFeeEstimate.IsAccurate && !fees.IsAccurate)
+ else
+ {
+ if (RpcFeeProvider.LastAllFeeEstimate?.IsAccurate is true && !RpcFeeProvider.InError)
{
- // If the fee was accurate and the new fee is inaccurate, then we should leave the fees alone.
+ // If user's full node is properly serving data, then we don't care about the backend.
return;
}
- else if (sender is RpcFeeProvider)
+ else
{
- // If the fee is coming from the user's full node, then set it.
+ if (syncer.BackendStatus == BackendStatus.Connected && fees.IsAccurate)
+ {
+ // If the backend is properly serving accurate data then, this is the best we got.
notify = SetAllFeeEstimate(fees);
}
- else if (sender is WasabiSynchronizer && RpcFeeProvider is null)
+ else
{
- // If fee is coming from the the backend and user doesn't use a full node, then set the fees.
- notify = SetAllFeeEstimate(fees);
+ // If neither user's full node, nor backend is ready, then let's try our best effort figuring out which data looks better:
+ notify = SetAllFeeEstimateIfLooksBetter(fees);
+ }
}
- else if (sender is WasabiSynchronizer && RpcFeeProvider is not null && RpcFeeProvider.InError is true)
+ }
+ }
+ else if (sender is RpcFeeProvider rpcProvider)
+ {
+ if (fees.IsAccurate && !rpcProvider.InError)
{
- // If fee is coming from the the backend, user uses a full node, but it doesn't provide fees, then set the fees.
+ // If user's full node is properly serving data, we're done here.
notify = SetAllFeeEstimate(fees);
}
+ else
+ {
+ if (Synchronizer.BackendStatus == BackendStatus.Connected)
+ {
+ // If the user's full node isn't ready, but the backend is, then let's leave it to the backend.
+ return;
+ }
+ else
+ {
+ // If neither user's full node, nor backend is ready, then let's try our best effort figuring out which data looks better:
+ notify = SetAllFeeEstimateIfLooksBetter(fees);
+ }
+ }
+ }
}
if (notify)
@@ -98,8 +119,21 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
var from = fees.Estimations.First();
var to = fees.Estimations.Last();
Logger.LogInfo($"{accuracy} fee rates are acquired from {sender?.GetType()?.Name} ranging from target {from.Key} at {from.Value} sat/b to target {to.Key} at {to.Value} sat/b.");
- AllFeeEstimateChanged?.Invoke(this, AllFeeEstimate!);
+ AllFeeEstimateChanged?.Invoke(this, fees);
+ }
+ }
+
+ /// <returns>True if changed.</returns>
+ private bool SetAllFeeEstimateIfLooksBetter(AllFeeEstimate? fees)
+ {
+ var current = AllFeeEstimate;
+ if (fees is null
+ || fees == current
+ || current is not null && ((!fees.IsAccurate && current.IsAccurate) || fees.Estimations.Count <= current.Estimations.Count))
+ {
+ return false;
}
+ return SetAllFeeEstimate(fees);
}
/// <returns>True if changed.</returns>

ADD
WalletWasabi/Blockchain/Statistics/HybridBestHeightProvider.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WalletWasabi.Backend.Models.Responses;
+using WalletWasabi.BitcoinCore.Monitoring;
+using WalletWasabi.Logging;
+using WalletWasabi.Models;
+using WalletWasabi.Services;
+
+namespace WalletWasabi.Blockchain.Statistics
+{
+ /// <summary>
+ /// Manages multiple best height sources. Returns the best one.
+ /// Prefers local full node, as long as it's synchronized.
+ /// </summary>
+ public class HybridBestHeightProvider : IDisposable
+ {
+ private volatile bool _disposedValue = false; // To detect redundant calls
+
+ public HybridBestHeightProvider(WasabiSynchronizer synchronizer, RpcMonitor? rpcMonitor)
+ {
+ Synchronizer = synchronizer;
+ RpcMonitor = rpcMonitor;
+
+ Synchronizer.ResponseArrived += Synchronizer_ResponseArrived;
+
+ if (RpcMonitor is not null)
+ {
+ RpcMonitor.RpcStatusChanged += RpcMonitor_RpcStatusChanged;
+ }
+
+ var rpcStatus = RpcMonitor?.RpcStatus;
+ if (rpcStatus is not null && rpcStatus.Success)
+ {
+ SetBestHeightIfLooksBetter((int)rpcStatus.Headers);
+ }
+ SetBestHeightIfLooksBetter(Synchronizer.LastResponse?.BestHeight);
+ }
+
+ public event EventHandler<int>? BestHeightChanged;
+
+ public WasabiSynchronizer Synchronizer { get; }
+ public RpcMonitor? RpcMonitor { get; }
+ private object Lock { get; } = new object();
+ public int? BestHeight { get; private set; }
+
+ private void Synchronizer_ResponseArrived(object? sender, SynchronizeResponse response)
+ {
+ OnBestHeightArrived(sender, response.BestHeight);
+ }
+
+ private void RpcMonitor_RpcStatusChanged(object? sender, RpcStatus status)
+ {
+ OnBestHeightArrived(sender, (int)status.Headers);
+ }
+
+ private void OnBestHeightArrived(object? sender, int height)
+ {
+ var notify = false;
+ lock (Lock)
+ {
+ if (BestHeight is null)
+ {
+ // If it wasn't set before, then set it regardless everything.
+ notify = SetBestHeight(height);
+ }
+ else if (sender is WasabiSynchronizer)
+ {
+ if (RpcMonitor is null)
+ {
+ // If user doesn't use full node, then set it, this is the best we got.
+ notify = SetBestHeight(height);
+ }
+ else
+ {
+ if (RpcMonitor.RpcStatus.Success)
+ {
+ // If user's full node is properly serving data, then we don't care about the backend.
+ return;
+ }
+ else
+ {
+ if (Synchronizer.BackendStatus == BackendStatus.Connected)
+ {
+ // If the backend is properly serving accurate data then, this is the best we got.
+ notify = SetBestHeight(height);
+ }
+ else
+ {
+ // If neither user's full node, nor backend is ready, then let's try our best effort figuring out which data looks better:
+ notify = SetBestHeightIfLooksBetter(height);
+ }
+ }
+ }
+ }
+ else if (sender is RpcMonitor rpcMonitor)
+ {
+ if (rpcMonitor.RpcStatus.Success)
+ {
+ // If user's full node is properly serving data, we're done here.
+ notify = SetBestHeight(height);
+ }
+ else
+ {
+ if (Synchronizer.BackendStatus == BackendStatus.Connected)
+ {
+ // If the user's full node isn't ready, but the backend is, then let's leave it to the backend.
+ return;
+ }
+ else
+ {
+ // If neither user's full node, nor backend is ready, then let's try our best effort figuring out which data looks better:
+ notify = SetBestHeightIfLooksBetter(height);
+ }
+ }
+ }
+ //else if (sender is WasabiSynchronizer && RpcMonitor is null)
+ //{
+ // // If it is coming from the the backend and user doesn't use a full node, then set the fees.
+ // notify = SetBestHeight(height);
+ //}
+ //else if (sender is WasabiSynchronizer && RpcMonitor?.InError is true)
+ //{
+ // // If data is coming from the the backend, user uses a full node, but it doesn't provide data, then set it.
+ // notify = SetBestHeight(height);
+ //}
+ //else if (sender is WasabiSynchronizer && RpcMonitor?.IsSynchronized is false)
+ //{
+ // // If data is coming from the the backend, user uses a full node, but it isn't synchronized yet, then set it.
+ // notify = SetBestHeight(height);
+ //}
+ //else if (sender is RpcMonitor && RpcMonitor.IsSynchronized is true)
+ //{
+ // // If our RPC is synchronized, then it probably has its height reight.
+ // notify = SetBestHeight(height);
+ //}
+ //else if (sender is RpcMonitor && Synchronizer.BackendStatus != BackendStatus.Connected)
+ //{
+ // // If backend isn't connected, then even if non synchrnonized data comes from RPC, let's believe it.
+ // notify = SetBestHeight(height);
+ //}
+ }
+
+ if (notify)
+ {
+ Logger.LogInfo($"Best blockchain height is acquired from {sender?.GetType()?.Name}: {height}.");
+ BestHeightChanged?.Invoke(this, height);
+ }
+ }
+
+ /// <returns>True if changed.</returns>
+ private bool SetBestHeightIfLooksBetter(int? height)
+ {
+ var current = BestHeight;
+ if (height is null || height <= current)
+ {
+ return false;
+ }
+ return SetBestHeight(height.Value);
+ }
+
+ /// <returns>True if changed.</returns>
+ private bool SetBestHeight(int height)
+ {
+ if (BestHeight == height)
+ {
+ return false;
+ }
+ BestHeight = height;
+ return true;
+ }
+
+ #region IDisposable Support
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ Synchronizer.ResponseArrived -= Synchronizer_ResponseArrived;
+
+ if (RpcMonitor is not null)
+ {
+ RpcMonitor.RpcStatusChanged -= RpcMonitor_RpcStatusChanged;
+ }
+ }
+
+ _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);
+ }
+
+ #endregion IDisposable Support
+ }
+}

Create HybridBestHeightProvider