-
Notifications
You must be signed in to change notification settings - Fork 4
Add ForkWatchJob #4
base: master
Are you sure you want to change the base?
Changes from 5 commits
1beab19
76af463
c2ca160
4b10716
a7dad98
6fc9eb7
4969f5d
c0754b7
422c5be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -277,4 +277,6 @@ __pycache__/ | |
errorlogs.txt | ||
version.txt | ||
*launchSettings.json | ||
AuditZips/ | ||
AuditZips/ | ||
|
||
.vscode |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
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<IConfiguration>(), | ||
dataExtractor = serviceProvider.GetService<IDataExtractorService>(), | ||
logger = serviceProvider.GetService<ILogger<ForkWatchJob>>(), | ||
mapper = serviceProvider.GetService<IMapper>(), | ||
cache = serviceProvider.GetService<IMemoryCache>(), | ||
notificationManager = serviceProvider.GetService<INotificationManager>(), | ||
}; | ||
executor.Execute(); | ||
} | ||
} | ||
|
||
class VirtualCheckpoint | ||
{ | ||
public int Height; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use property, not field for DTOs. And convention follows to end with DTO in the name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
public string Hash; | ||
} | ||
|
||
class WatchJobExecutor | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move to separate .cs file , seems like Service, And convention follows to end with Service in the name. |
||
{ | ||
const int VIRTUAL_FINALIZE_BLOCKS = 3; | ||
|
||
public IConfiguration config; | ||
public IDataExtractorService dataExtractor; | ||
public ILogger<ForkWatchJob> logger; | ||
public IMapper mapper; | ||
public IMemoryCache cache; | ||
public RpcConfig rpcConfig; | ||
public INotificationManager notificationManager; | ||
|
||
public void Execute() | ||
{ | ||
rpcConfig = JobCommon.GetRpcConfig(config); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Config can be injected into the method and gathered inside of the job. |
||
// compare chain tips | ||
var lastSeenTips = cache.GetOrCreate<ChainTip[]>(Constants.LastSeenTipKey, entry => null); | ||
ChainTip[] tips = null; | ||
var shouldBacktrace = false; | ||
Handle(Rpc<GetChainTipsResult>("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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Active TODO |
||
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add additional information, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
} | ||
|
||
// short-circurit: no new block found | ||
if (lastSeenTips != null && lastSeenTips[0].Hash == tips[0].Hash) | ||
{ | ||
return; | ||
} | ||
|
||
// check virtual checkpoint rolled-back | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comments should explain why something is done, not how or what. Comment convention uses upper first letter. |
||
var lastCheckpoint = cache.GetOrCreate<VirtualCheckpoint>( | ||
Constants.VirtualCheckpointKey, entry => null); | ||
if (lastCheckpoint != null) | ||
{ | ||
bool foundReorg = false; | ||
// check if the checkpoint is still in the main chain | ||
Handle(Rpc<RpcResult>("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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Active TODO |
||
} | ||
// update virtual checkpoint | ||
var height = tips[0].Height; | ||
var toFinalize = height - VIRTUAL_FINALIZE_BLOCKS; | ||
logger.LogWarning("getblockhash({0})", toFinalize); | ||
Handle(Rpc<RpcResult>("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<T> Rpc<T>(string name, params object[] args) where T : class | ||
{ | ||
return dataExtractor.RpcCall<T>(rpcConfig, name, args); | ||
} | ||
|
||
Result<RpcResult> Rpc(string name, params object[] args) | ||
{ | ||
return Rpc<RpcResult>(name, args); | ||
} | ||
|
||
void Handle<T>(Result<T> result, Action<T> 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<string>(dictA.Keys); | ||
var hashesB = new HashSet<string>(dictB.Keys); | ||
var added = new HashSet<string>(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<RpcBlockInfo> BacktraceBlocks(string tipHash, int n = 0) | ||
{ | ||
var blocks = new List<RpcBlockInfo>(); | ||
var h = tipHash; | ||
while (true) | ||
{ | ||
var end = false; | ||
Handle(Rpc<RpcBlockInfo>("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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using Newtonsoft.Json; | ||
|
||
namespace CheckerApi.Models.Rpc | ||
{ | ||
public class GetChainTipsResult | ||
{ | ||
[JsonProperty("result")] | ||
public ChainTip[] Result { get; set; } | ||
} | ||
|
||
public class ChainTip | ||
{ | ||
[JsonProperty("height")] | ||
public int Height { get; set; } | ||
|
||
[JsonProperty("hash")] | ||
public string Hash { get; set; } | ||
|
||
[JsonProperty("branchlen")] | ||
public int BranchLen { get; set; } | ||
|
||
[JsonProperty("status")] | ||
public string Status { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using Newtonsoft.Json; | ||
|
||
namespace CheckerApi.Models.Rpc | ||
{ | ||
public class Transaction | ||
{ | ||
[JsonProperty("txid")] | ||
public string TxId { get; set; } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move to separate .cs file