Skip to content
This repository has been archived by the owner on May 23, 2021. It is now read-only.

Add ForkWatchJob #4

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,6 @@ __pycache__/
errorlogs.txt
version.txt
*launchSettings.json
AuditZips/
AuditZips/

.vscode
199 changes: 199 additions & 0 deletions CheckerApi/Jobs/ForkWatchJob.cs
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
Copy link
Owner

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

{
public int Height;
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

public string Hash;
}

class WatchJobExecutor
Copy link
Owner

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 , 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);
Copy link
Owner

Choose a reason for hiding this comment

The 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
Copy link
Owner

Choose a reason for hiding this comment

The 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");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add additional information, bad tip is not descriptive enough to determine a potential problem.

Copy link
Author

Choose a reason for hiding this comment

The 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
Copy link
Owner

Choose a reason for hiding this comment

The 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"
Copy link
Owner

Choose a reason for hiding this comment

The 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;
}
}
}
11 changes: 1 addition & 10 deletions CheckerApi/Jobs/NodeJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,7 @@ public override void Execute(JobDataMap data, IServiceProvider serviceProvider)
var logger = serviceProvider.GetService<ILogger<NodeJob>>();
var mapper = serviceProvider.GetService<IMapper>();

var rpcConfig = new RpcConfig()
{
Url = config.GetValue<string>("Node:Url"),
Port = config.GetValue<int>("Node:RpcPort"),
Credentials = new NetworkCredential()
{
UserName = config.GetValue<string>("Node:RpcUser"),
Password = config.GetValue<string>("Node:RpcPass")
}
};
var rpcConfig = JobCommon.GetRpcConfig(config);

var cache = serviceProvider.GetService<IMemoryCache>();
var difficultyResult = dataExtractor.RpcCall(rpcConfig, "getdifficulty");
Expand Down
25 changes: 25 additions & 0 deletions CheckerApi/Models/Rpc/GetChainTipsResult.cs
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; }
}
}
20 changes: 19 additions & 1 deletion CheckerApi/Models/Rpc/RpcBlockInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace CheckerApi.Models.Rpc
{
public class RpcBlockInfo
public class RpcBlockInfoBase
{
[JsonProperty("hash")]
public string Hash { get; set; }
Expand All @@ -15,6 +15,24 @@ public class RpcBlockInfo

[JsonProperty("previousblockhash")]
public string PreviousBlockHash { get; set; }

[JsonProperty("chainwork")]
public string ChainWork { get; set; }

[JsonProperty("confirmations")]
public int Confirmations { get; set; }
}

public class RpcBlockInfo : RpcBlockInfoBase
{
[JsonProperty("tx")]
public string[] Tx { get; set; }
}

public class RpcBlockInfoVerbose : RpcBlockInfoBase
{
[JsonProperty("tx")]
public Transaction[] Tx { get; set; }
}

public class RpcBlockResult
Expand Down
10 changes: 10 additions & 0 deletions CheckerApi/Models/Rpc/Transaction.cs
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; }
}
}
13 changes: 13 additions & 0 deletions CheckerApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ public static void Main(string[] args)
startAt: DateTimeOffset.UtcNow.AddSeconds(2)
);
}

var forkWatchEnabled = config.GetValue<bool>("ForkWatch:Enable");
if (forkWatchEnabled)
{
scheduler.AddJob<ForkWatchJob>(
host,
tb => tb.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever()
),
startAt: DateTimeOffset.UtcNow.AddSeconds(3)
);
}
}
})
.Run();
Expand Down
23 changes: 19 additions & 4 deletions CheckerApi/Services/DataExtractorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CheckerApi.Models.Rpc;
using CheckerApi.Services.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RestSharp;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -41,7 +42,21 @@ public Result<IEnumerable<string>> GetData(string url, string req, string patter
return Result<IEnumerable<string>>.Ok(match.Groups.Select(g => g.Value));
}

public Result<string> RpcCall(RpcConfig config, string method, params string[] parameters)
private JArray ObjectToJArray(object[] objs)
{
var a = new JArray();
foreach(var o in objs)
{
JValue val;
if (o is int) val = new JValue((int)o);
else if (o is string) val = new JValue((string)o);
else val = new JValue(o.ToString());
a.Add(val);
}
return a;
}

public Result<string> RpcCall(RpcConfig config, string method, params object[] parameters)
{
var result = this.RpcCall<RpcResult>(config, method, parameters);
if (result.IsSuccess())
Expand All @@ -52,16 +67,16 @@ public Result<string> RpcCall(RpcConfig config, string method, params string[] p
return Result<string>.Fail(result.Messages.ToArray());
}

public Result<T> RpcCall<T>(RpcConfig config, string method, params string[] parameters) where T : class
public Result<T> RpcCall<T>(RpcConfig config, string method, params object[] parameters) where T : class
{
var client = new RestClient($"{config.Url}:{config.Port}");
var request = new RestRequest(string.Empty, Method.POST)
{
Credentials = config.Credentials
};

var pars = string.Join(",", parameters.Select(p => $"\"{p}\""));
request.AddParameter("text/xml", $"{{\"jsonrpc\":\"1.0\",\"id\":\"alert-bot\",\"method\":\"{method}\",\"params\":[{pars}]}}", ParameterType.RequestBody);
var pars = ObjectToJArray(parameters).ToString();
request.AddParameter("text/xml", $"{{\"jsonrpc\":\"1.0\",\"id\":\"alert-bot\",\"method\":\"{method}\",\"params\":{pars}}}", ParameterType.RequestBody);
var response = client.Execute(request);

if (response.StatusCode != HttpStatusCode.OK)
Expand Down
4 changes: 2 additions & 2 deletions CheckerApi/Services/Interfaces/IDataExtractorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace CheckerApi.Services.Interfaces
public interface IDataExtractorService
{
Result<IEnumerable<string>> GetData(string url, string req, string pattern);
Result<T> RpcCall<T>(RpcConfig config, string method, params string[] parameters) where T : class;
Result<string> RpcCall(RpcConfig config, string method, params string[] parameters);
Result<T> RpcCall<T>(RpcConfig config, string method, params object[] parameters) where T : class;
Result<string> RpcCall(RpcConfig config, string method, params object[] parameters);
}
}
2 changes: 2 additions & 0 deletions CheckerApi/Utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Loading