Refactor block downloading code
ADD
WalletWasabi/Wallets/FileSystemBlockRepository.cs
+using NBitcoin;
+using NBitcoin.DataEncoders;
+using Nito.AsyncEx;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Logging;
+
+namespace WalletWasabi.Wallets
+{
+ /// <summary>
+ /// FileSystemBlocksRepository is a blocks repository that keep the blocks in the file system.
+ /// </summary>
+ public class FileSystemBlockRepository : IRepository<uint256, Block>
+ {
+ public FileSystemBlockRepository(string blocksFolderPath, Network network)
+ {
+ BlocksFolderPath = blocksFolderPath;
+ Network = network;
+ CreateFolders();
+ }
+
+ public string BlocksFolderPath { get; }
+ public Network Network { get; }
+ private AsyncLock BlockFolderLock { get; } = new AsyncLock();
+
+ /// <summary>
+ /// Gets a bitcoin block from the file system.
+ /// </summary>
+ /// <param name="hash">The block's hash that identifies the requested block.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The requested bitcoin block.</returns>
+ public async Task<Block> GetAsync(uint256 hash, CancellationToken cancellationToken)
+ {
+ // Try get the block
+ Block block = null;
+ using (await BlockFolderLock.LockAsync())
+ {
+ var encoder = new HexEncoder();
+ var filePath = Path.Combine(BlocksFolderPath, hash.ToString());
+ if (File.Exists(filePath))
+ {
+ try
+ {
+ var blockBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
+ block = Block.Load(blockBytes, Network);
+ }
+ catch
+ {
+ // In case the block file is corrupted and we get an EndOfStreamException exception
+ // Ignore any error and continue to re-downloading the block.
+ Logger.LogDebug($"Block {hash} file corrupted, deleting file and block will be re-downloaded.");
+ File.Delete(filePath);
+ }
+ }
+ }
+
+ return block;
+ }
+
+ /// <summary>
+ /// Saves a bitcoin block in the file system.
+ /// </summary>
+ /// <param name="block">The block to be persisted in the file system.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ public async Task SaveAsync(Block block, CancellationToken cancellationToken)
+ {
+ var path = Path.Combine(BlocksFolderPath, block.GetHash().ToString());
+ if (!File.Exists(path))
+ {
+ using (await BlockFolderLock.LockAsync())
+ {
+ if (!File.Exists(path))
+ {
+ await File.WriteAllBytesAsync(path, block.ToBytes());
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Deletes a bitcoin block from the file system.
+ /// </summary>
+ /// <param name="hash">The block's hash that identifies the requested block.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ public async Task RemoveAsync(uint256 hash, CancellationToken cancellationToken)
+ {
+ try
+ {
+ using (await BlockFolderLock.LockAsync())
+ {
+ var filePaths = Directory.EnumerateFiles(BlocksFolderPath);
+ var fileNames = filePaths.Select(Path.GetFileName);
+ var hashes = fileNames.Select(x => new uint256(x));
+
+ if (hashes.Contains(hash))
+ {
+ File.Delete(Path.Combine(BlocksFolderPath, hash.ToString()));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning(ex);
+ }
+ }
+
+ /// <summary>
+ /// Returns the number of blocks available in the file system. (for testing only)
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The requested bitcoin block.</returns>
+ public async Task<int> CountAsync(CancellationToken cancellationToken)
+ {
+ using (await BlockFolderLock.LockAsync())
+ {
+ return Directory.EnumerateFiles(BlocksFolderPath).Count();
+ }
+ }
+
+ private void CreateFolders()
+ {
+ if (Directory.Exists(BlocksFolderPath))
+ {
+ if (Network == Network.RegTest)
+ {
+ Directory.Delete(BlocksFolderPath, true);
+ Directory.CreateDirectory(BlocksFolderPath);
+ }
+ }
+ else
+ {
+ Directory.CreateDirectory(BlocksFolderPath);
+ }
+ }
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Wallets/IBlocksProvider.cs
+using System.Threading;
+using System.Threading.Tasks;
+using NBitcoin;
+
+namespace WalletWasabi.Wallets
+{
+ /// <summary>
+ /// IBlocksProvider is an abstraction for types that can return blocks.
+ /// </summary>
+ public interface IBlocksProvider
+ {
+ Task<Block> GetBlockAsync(uint256 hash, CancellationToken cancel);
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Wallets/IRepository.cs
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.Wallets
+{
+ /// <summary>
+ /// IRepository is a generic abstraction of a repository pattern
+ /// </summary>
+ public interface IRepository<TID, TElement>
+ {
+ Task<TElement> GetAsync(TID id, CancellationToken cancel);
+ Task SaveAsync(TElement element, CancellationToken cancel);
+ Task RemoveAsync(TID id, CancellationToken cancel);
+ Task<int> CountAsync(CancellationToken cancel);
+ }
+}
\ No newline at end of file

ADD
WalletWasabi/Wallets/P2pBlocksProvider.cs
+using NBitcoin;
+using NBitcoin.Protocol;
+using NBitcoin.Protocol.Behaviors;
+using System;
+using System.Linq;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.BitcoinCore;
+using WalletWasabi.Helpers;
+using WalletWasabi.Logging;
+using WalletWasabi.Models;
+using WalletWasabi.Services;
+
+namespace WalletWasabi.Wallets
+{
+ /// <summary>
+ /// P2pBlocksProvider is a blocks provider that provides blocks
+ /// from bitcoin nodes using the P2P bitcoin protocol.
+ /// </summary>
+ public class P2pBlocksProvider : IBlocksProvider
+ {
+ private Node _localBitcoinCoreNode = null;
+
+ public P2pBlocksProvider(NodesGroup nodes, CoreNode coreNode, WasabiSynchronizer syncer, ServiceConfiguration serviceConfiguration, Network network)
+ {
+ Nodes = nodes;
+ CoreNode = coreNode;
+ Synchronizer = syncer;
+ ServiceConfiguration = serviceConfiguration;
+ Network = network;
+ }
+
+ public static event EventHandler<bool> DownloadingBlockChanged;
+ public NodesGroup Nodes { get; }
+ public CoreNode CoreNode { get; }
+ public WasabiSynchronizer Synchronizer { get; }
+ public ServiceConfiguration ServiceConfiguration { get; }
+ public Network Network { get; }
+
+ public Node LocalBitcoinCoreNode
+ {
+ get
+ {
+ if (Network == Network.RegTest)
+ {
+ return Nodes.ConnectedNodes.First();
+ }
+
+ return _localBitcoinCoreNode;
+ }
+ private set => _localBitcoinCoreNode = value;
+ }
+
+ private int NodeTimeouts { get; set; }
+
+ /// <summary>
+ /// Gets a bitcoin block from bitcoin nodes using the p2p bitcoin protocol.
+ /// If a CoreNode is available it fetches the blocks using the rpc interface.
+ /// </summary>
+ /// <param name="hash">The block's hash that identifies the requested block.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The requested bitcoin block.</returns>
+ public async Task<Block> GetBlockAsync(uint256 hash, CancellationToken cancellationToken)
+ {
+ Block block = null;
+ try
+ {
+ DownloadingBlockChanged?.Invoke(null, true);
+
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ try
+ {
+ // Try to get block information from local running Core node first.
+ block = await TryDownloadBlockFromLocalNodeAsync(hash, cancellationToken);
+
+ if (block is { })
+ {
+ break;
+ }
+
+ // If no connection, wait, then continue.
+ while (Nodes.ConnectedNodes.Count == 0)
+ {
+ await Task.Delay(100, cancellationToken);
+ }
+
+ // Select a random node we are connected to.
+ Node node = Nodes.ConnectedNodes.RandomElement();
+ if (node is null || !node.IsConnected)
+ {
+ await Task.Delay(100, cancellationToken);
+ continue;
+ }
+
+ // Download block from selected node.
+ try
+ {
+ using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(RuntimeParams.Instance.NetworkNodeTimeout))) // 1/2 ADSL 512 kbit/s 00:00:32
+ {
+ using var lts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken);
+ block = await node.DownloadBlockAsync(hash, lts.Token);
+ }
+
+ // Validate block
+ if (!block.Check())
+ {
+ Logger.LogInfo($"Disconnected node: {node.RemoteSocketAddress}, because invalid block received.");
+ node.DisconnectAsync("Invalid block received.");
+ continue;
+ }
+
+ if (Nodes.ConnectedNodes.Count > 1) // To minimize risking missing unconfirmed transactions.
+ {
+ Logger.LogInfo($"Disconnected node: {node.RemoteSocketAddress}. Block downloaded: {block.GetHash()}.");
+ node.DisconnectAsync("Thank you!");
+ }
+
+ await NodeTimeoutsAsync(false);
+ }
+ catch (Exception ex) when (ex is OperationCanceledException || ex is TaskCanceledException || ex is TimeoutException)
+ {
+ Logger.LogInfo($"Disconnected node: {node.RemoteSocketAddress}, because block download took too long.");
+
+ await NodeTimeoutsAsync(true);
+
+ node.DisconnectAsync("Block download took too long.");
+ continue;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug(ex);
+ Logger.LogInfo($"Disconnected node: {node.RemoteSocketAddress}, because block download failed: {ex.Message}.");
+ node.DisconnectAsync("Block download failed.");
+ continue;
+ }
+
+ break; // If got this far, then we have the block and it's valid. Break.
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug(ex);
+ }
+ }
+ }
+ finally
+ {
+ DownloadingBlockChanged?.Invoke(null, false);
+ }
+
+ return block;
+ }
+
+ private async Task<Block> TryDownloadBlockFromLocalNodeAsync(uint256 hash, CancellationToken cancellationToken)
+ {
+ if (CoreNode?.RpcClient is null)
+ {
+ try
+ {
+ if (LocalBitcoinCoreNode is null || (!LocalBitcoinCoreNode.IsConnected && Network != Network.RegTest)) // If RegTest then we're already connected do not try again.
+ {
+ DisconnectDisposeNullLocalBitcoinCoreNode();
+ using var handshakeTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ handshakeTimeout.CancelAfter(TimeSpan.FromSeconds(10));
+ var nodeConnectionParameters = new NodeConnectionParameters()
+ {
+ ConnectCancellation = handshakeTimeout.Token,
+ IsRelay = false,
+ UserAgent = $"/Wasabi:{Constants.ClientVersion.ToString()}/"
+ };
+
+ // If an onion was added must try to use Tor.
+ // onlyForOnionHosts should connect to it if it's an onion endpoint automatically and non-Tor endpoints through clearnet/localhost
+ if (Synchronizer.WasabiClient.TorClient.IsTorUsed)
+ {
+ nodeConnectionParameters.TemplateBehaviors.Add(new SocksSettingsBehavior(Synchronizer.WasabiClient.TorClient.TorSocks5EndPoint, onlyForOnionHosts: true, networkCredential: null, streamIsolation: false));
+ }
+
+ var localEndPoint = ServiceConfiguration.BitcoinCoreEndPoint;
+ var localNode = await Node.ConnectAsync(Network, localEndPoint, nodeConnectionParameters);
+ try
+ {
+ Logger.LogInfo("TCP Connection succeeded, handshaking...");
+ localNode.VersionHandshake(Constants.LocalNodeRequirements, handshakeTimeout.Token);
+ var peerServices = localNode.PeerVersion.Services;
+
+ //if (!peerServices.HasFlag(NodeServices.Network) && !peerServices.HasFlag(NodeServices.NODE_NETWORK_LIMITED))
+ //{
+ // throw new InvalidOperationException("Wasabi cannot use the local node because it does not provide blocks.");
+ //}
+
+ Logger.LogInfo("Handshake completed successfully.");
+
+ if (!localNode.IsConnected)
+ {
+ throw new InvalidOperationException($"Wasabi could not complete the handshake with the local node and dropped the connection.{Environment.NewLine}" +
+ "Probably this is because the node does not support retrieving full blocks or segwit serialization.");
+ }
+ LocalBitcoinCoreNode = localNode;
+ }
+ catch (OperationCanceledException) when (handshakeTimeout.IsCancellationRequested)
+ {
+ Logger.LogWarning($"Wasabi could not complete the handshake with the local node. Probably Wasabi is not whitelisted by the node.{Environment.NewLine}" +
+ "Use \"whitebind\" in the node configuration. (Typically whitebind=127.0.0.1:8333 if Wasabi and the node are on the same machine and whitelist=1.2.3.4 if they are not.)");
+ throw;
+ }
+ }
+
+ // Get Block from local node
+ Block blockFromLocalNode = null;
+ // Should timeout faster. Not sure if it should ever fail though. Maybe let's keep like this later for remote node connection.
+ using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(64)))
+ {
+ blockFromLocalNode = await LocalBitcoinCoreNode.DownloadBlockAsync(hash, cts.Token);
+ }
+
+ // Validate retrieved block
+ if (!blockFromLocalNode.Check())
+ {
+ throw new InvalidOperationException("Disconnected node, because invalid block received!");
+ }
+
+ // Retrieved block from local node and block is valid
+ Logger.LogInfo($"Block acquired from local P2P connection: {hash}.");
+ return blockFromLocalNode;
+ }
+ catch (Exception ex)
+ {
+ DisconnectDisposeNullLocalBitcoinCoreNode();
+
+ if (ex is SocketException)
+ {
+ Logger.LogTrace("Did not find local listening and running full node instance. Trying to fetch needed block from other source.");
+ }
+ else
+ {
+ Logger.LogWarning(ex);
+ }
+ }
+ }
+ else
+ {
+ try
+ {
+ var block = await CoreNode.RpcClient.GetBlockAsync(hash).ConfigureAwait(false);
+ Logger.LogInfo($"Block acquired from RPC connection: {hash}.");
+ return block;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning(ex);
+ }
+ }
+
+ return null;
+ }
+
+ private void DisconnectDisposeNullLocalBitcoinCoreNode()
+ {
+ if (LocalBitcoinCoreNode != null)
+ {
+ try
+ {
+ LocalBitcoinCoreNode?.Disconnect();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug(ex);
+ }
+ finally
+ {
+ try
+ {
+ LocalBitcoinCoreNode?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug(ex);
+ }
+ finally
+ {
+ LocalBitcoinCoreNode = null;
+ Logger.LogInfo("Local Bitcoin Core node disconnected.");
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Current timeout used when downloading a block from the remote node. It is defined in seconds.
+ /// </summary>
+ private async Task NodeTimeoutsAsync(bool increaseDecrease)
+ {
+ if (increaseDecrease)
+ {
+ NodeTimeouts++;
+ }
+ else
+ {
+ NodeTimeouts--;
+ }
+
+ var timeout = RuntimeParams.Instance.NetworkNodeTimeout;
+
+ // If it times out 2 times in a row then increase the timeout.
+ if (NodeTimeouts >= 2)
+ {
+ NodeTimeouts = 0;
+ timeout *= 2;
+ }
+ else if (NodeTimeouts <= -3) // If it does not time out 3 times in a row, lower the timeout.
+ {
+ NodeTimeouts = 0;
+ timeout = (int)Math.Round(timeout * 0.7);
+ }
+
+ // Sanity check
+ if (timeout < 32)
+ {
+ timeout = 32;
+ }
+ else if (timeout > 600)
+ {
+ timeout = 600;
+ }
+
+ if (timeout == RuntimeParams.Instance.NetworkNodeTimeout)
+ {
+ return;
+ }
+
+ RuntimeParams.Instance.NetworkNodeTimeout = timeout;
+ await RuntimeParams.Instance.SaveAsync();
+
+ Logger.LogInfo($"Current timeout value used on block download is: {timeout} seconds.");
+ }
+ }
+}

Refactor block downloading code