diff --git a/CheckerApi/Jobs/ForkWatchJob.cs b/CheckerApi/Jobs/ForkWatchJob.cs new file mode 100644 index 0000000..a0bb47c --- /dev/null +++ b/CheckerApi/Jobs/ForkWatchJob.cs @@ -0,0 +1,211 @@ +using AutoMapper; +using CheckerApi.Extensions; +using CheckerApi.Models; +using CheckerApi.Models.Config; +using CheckerApi.Models.DTO; +using CheckerApi.Models.Rpc; +using CheckerApi.Services.Interfaces; +using CheckerApi.Utils; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Quartz; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace CheckerApi.Jobs +{ + [DisallowConcurrentExecution] + public class ForkWatchJob : Job + { + public override void Execute(JobDataMap data, IServiceProvider serviceProvider) + { + var executor = new WatchJobExecutor() + { + config = serviceProvider.GetService(), + dataExtractor = serviceProvider.GetService(), + logger = serviceProvider.GetService>(), + mapper = serviceProvider.GetService(), + cache = serviceProvider.GetService(), + notificationManager = serviceProvider.GetService(), + }; + executor.Execute(); + } + // private Result GetBlockInfo(RpcConfig rpcConfig, IDataExtractorService dataExtractor, IMapper mapper, string blockHash) + // { + // var blockResult = dataExtractor.RpcCall(rpcConfig, "getblock", blockHash); + // if (blockResult.HasFailed()) + // { + // return Result.Fail(blockResult.Messages.ToArray()); + // } + + // var block = mapper.Map(blockResult.Value); + // return Result.Ok(block); + // } + + } + + class VirtualCheckpoint + { + public int Height; + public string Hash; + } + + class WatchJobExecutor + { + const int VIRTUAL_FINALIZE_BLOCKS = 3; + + public IConfiguration config; + public IDataExtractorService dataExtractor; + public ILogger logger; + public IMapper mapper; + public IMemoryCache cache; + public RpcConfig rpcConfig; + public INotificationManager notificationManager; + + public void Execute() + { + rpcConfig = JobCommon.GetRpcConfig(config); + // compare chain tips + var lastSeenTips = cache.GetOrCreate(Constants.LastSeenTipKey, entry => null); + ChainTip[] tips = null; + var shouldBacktrace = false; + Handle(Rpc("getchaintips"), r => + { + tips = r.Result; + var shouldSend = true; + var desc = "ForkWatch started"; + if (lastSeenTips != null) + { + (shouldSend, desc) = DiffTip(lastSeenTips, tips); + } + if (shouldSend) + { + shouldBacktrace = true; + var url = ""; // TODO: upload to pastbin and get url + var message = $"{desc}\n{url}"; + notificationManager.TriggerHook(message); + } + + cache.Set(Constants.LastSeenTipKey, tips); + }); + + if (tips == null || tips.Length == 0) + { + logger.LogWarning("ForkWatch: Got bad tip"); + } + + // short-circurit: no new block found + if (lastSeenTips != null && lastSeenTips[0].Hash == tips[0].Hash) + { + return; + } + + // check virtual checkpoint rolled-back + var lastCheckpoint = cache.GetOrCreate( + Constants.VirtualCheckpointKey, entry => null); + if (lastCheckpoint != null) + { + bool foundReorg = false; + // check if the checkpoint is still in the main chain + Handle(Rpc("getblockhash", lastCheckpoint.Height), r => + { + var hash = r.Result; + foundReorg = (hash != lastCheckpoint.Hash); + var message = $"ForkWatch: Virtual checkpoint {lastCheckpoint.Hash} at height {lastCheckpoint.Height} replaced by {hash}"; + notificationManager.TriggerHook(message); + }); + // TODO: if foundReorg, move to "PREPARE" + } + // update virtual checkpoint + var height = tips[0].Height; + var toFinalize = height - VIRTUAL_FINALIZE_BLOCKS; + logger.LogWarning("getblockhash({0})", toFinalize); + Handle(Rpc("getblockhash", toFinalize), r => + { + var hash = r.Result; + cache.Set(Constants.VirtualCheckpointKey, new VirtualCheckpoint() + { + Hash = hash, + Height = toFinalize + }); + notificationManager.TriggerHook( + $"ForkWatch: new checkpoint {hash} at {toFinalize} ({-VIRTUAL_FINALIZE_BLOCKS}) tip: {height}"); + }); + } + + Result Rpc(string name, params object[] args) where T : class + { + return dataExtractor.RpcCall(rpcConfig, name, args); + } + + Result Rpc(string name, params object[] args) + { + return Rpc(name, args); + } + + void Handle(Result result, Action action) + { + if (result.HasFailed()) + { + logger.LogError(result.Messages.ToCommaSeparated()); + return; + } + action(result.Value); + } + + (bool, string) DiffTip(ChainTip[] a, ChainTip[] b) + { + var dictA = (from t in a where t.Status != "active" select t) + .ToDictionary(t => t.Hash, t => t); + var dictB = (from t in b where t.Status != "active" && t.Status != "headers-only" select t) + .ToDictionary(t => t.Hash, t => t); + + var hashesA = new HashSet(dictA.Keys); + var hashesB = new HashSet(dictB.Keys); + var added = new HashSet(hashesB); + added.ExceptWith(hashesA); + + if (!added.Any()) + { + return (false, null); + } + + var branches = SimpleJson.SimpleJson.SerializeObject( + (from h in added select dictB[h]).ToList()); + return (true, $"New branch: {branches}"); + } + + // n == 0: backtrace until meet main chain + // n > 0: backtrace n blocks + List BacktraceBlocks(string tipHash, int n = 0) + { + var blocks = new List(); + var h = tipHash; + while (true) + { + var end = false; + Handle(Rpc("getblock", h), b => + { + logger.LogInformation("Backtrace: {0} {1}", h, b.Confirmations); + if (n == 0 && b.Confirmations >= 0) + { + end = true; + return; + } + blocks.Append(b); + h = b.PreviousBlockHash; + if (blocks.Count == n) + { + end = true; + } + }); + if (end) break; + } + return blocks; + } + } +} \ No newline at end of file diff --git a/CheckerApi/Program.cs b/CheckerApi/Program.cs index 9a55375..e9eae19 100644 --- a/CheckerApi/Program.cs +++ b/CheckerApi/Program.cs @@ -106,6 +106,19 @@ public static void Main(string[] args) startAt: DateTimeOffset.UtcNow.AddSeconds(2) ); } + + var forkWatchEnabled = config.GetValue("ForkWatch:Enable"); + if (forkWatchEnabled) + { + scheduler.AddJob( + host, + tb => tb.WithSimpleSchedule(x => x + .WithIntervalInSeconds(10) + .RepeatForever() + ), + startAt: DateTimeOffset.UtcNow.AddSeconds(3) + ); + } } }) .Run(); diff --git a/CheckerApi/Utils/Constants.cs b/CheckerApi/Utils/Constants.cs index aab5265..2ba89f0 100644 --- a/CheckerApi/Utils/Constants.cs +++ b/CheckerApi/Utils/Constants.cs @@ -6,5 +6,7 @@ public static class Constants public static string BtcBtgPriceKey { get; } = "BtcBtgPriceKey"; public static string DifficultyKey { get; } = "DifficultyKey"; public static string BlocksInfoKey { get; } = "BlocksInfoKey"; + public static string LastSeenTipKey { get; } = "LastSeenTipKey"; + public static string VirtualCheckpointKey { get; } = "VirtualCheckpointKey"; } }