cs-codex-dist-tests/Framework/NethereumWorkflow/BlockTimeFinder.cs

206 lines
6.5 KiB
C#

using Nethereum.RPC.Eth.DTOs;
using Nethereum.Web3;
using Utils;
namespace NethereumWorkflow
{
public class BlockTimeFinder
{
private class BlockTimeEntry
{
public BlockTimeEntry(ulong blockNumber, DateTime utc)
{
BlockNumber = blockNumber;
Utc = utc;
}
public ulong BlockNumber { get; }
public DateTime Utc { get; }
}
private const ulong FetchRange = 6;
private const int MaxEntries = 1024;
private readonly Web3 web3;
private static readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
public BlockTimeFinder(Web3 web3)
{
this.web3 = web3;
}
public ulong GetHighestBlockNumberBefore(DateTime moment)
{
AssertMomentIsInPast(moment);
Initialize();
var closestBefore = FindClosestBeforeEntry(moment);
var closestAfter = FindClosestAfterEntry(moment);
if (closestBefore.Utc < moment &&
closestAfter.Utc > moment &&
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
{
return closestBefore.BlockNumber;
}
FetchBlocksAround(moment);
return GetHighestBlockNumberBefore(moment);
}
public ulong GetLowestBlockNumberAfter(DateTime moment)
{
AssertMomentIsInPast(moment);
Initialize();
var closestBefore = FindClosestBeforeEntry(moment);
var closestAfter = FindClosestAfterEntry(moment);
if (closestBefore.Utc < moment &&
closestAfter.Utc > moment &&
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
{
return closestAfter.BlockNumber;
}
FetchBlocksAround(moment);
return GetLowestBlockNumberAfter(moment);
}
private void FetchBlocksAround(DateTime moment)
{
var timePerBlock = EstimateTimePerBlock();
EnsureRecentBlockIfNecessary(moment, timePerBlock);
var max = entries.Keys.Max();
var latest = entries[max];
var timeDifference = latest.Utc - moment;
double secondsDifference = Math.Abs(timeDifference.TotalSeconds);
double secondsPerBlock = timePerBlock.TotalSeconds;
double numberOfBlocksDifference = secondsDifference / secondsPerBlock;
var blockDifference = Convert.ToUInt64(numberOfBlocksDifference);
var fetchStart = (max - blockDifference) - (FetchRange / 2);
for (ulong i = 0; i < FetchRange; i++)
{
AddBlockNumber(fetchStart + i);
}
}
private void EnsureRecentBlockIfNecessary(DateTime moment, TimeSpan timePerBlock)
{
var max = entries.Keys.Max();
var latest = entries[max];
var maxRetry = 10;
while (moment > latest.Utc)
{
var newBlock = AddCurrentBlock();
if (newBlock.BlockNumber == latest.BlockNumber)
{
maxRetry--;
if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries.");
Thread.Sleep(timePerBlock);
}
}
}
private BlockTimeEntry AddBlockNumber(decimal blockNumber)
{
return AddBlockNumber(Convert.ToUInt64(blockNumber));
}
private BlockTimeEntry AddBlockNumber(ulong blockNumber)
{
if (entries.ContainsKey(blockNumber))
{
return entries[blockNumber];
}
if (entries.Count > MaxEntries)
{
entries.Clear();
Initialize();
}
var time = GetTimestampFromBlock(blockNumber);
var entry = new BlockTimeEntry(blockNumber, time);
entries.Add(blockNumber, entry);
return entry;
}
private TimeSpan EstimateTimePerBlock()
{
var min = entries.Keys.Min();
var max = entries.Keys.Max();
var minTime = entries[min].Utc;
var maxTime = entries[max].Utc;
var elapsedTime = maxTime - minTime;
double elapsedSeconds = elapsedTime.TotalSeconds;
double numberOfBlocks = max - min;
double secondsPerBlock = elapsedSeconds / numberOfBlocks;
return TimeSpan.FromSeconds(secondsPerBlock);
}
private void Initialize()
{
if (!entries.Any())
{
AddCurrentBlock();
AddBlockNumber(entries.Single().Key - 1);
}
}
private static void AssertMomentIsInPast(DateTime moment)
{
if (moment > DateTime.UtcNow) throw new Exception("Moment must be UTC and must be in the past.");
}
private BlockTimeEntry AddCurrentBlock()
{
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
var blockNumber = number.ToDecimal();
return AddBlockNumber(blockNumber);
}
private DateTime GetTimestampFromBlock(ulong blockNumber)
{
var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber)));
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime;
}
private BlockTimeEntry FindClosestBeforeEntry(DateTime moment)
{
var result = entries.Values.First();
var highestTime = result.Utc;
foreach (var entry in entries.Values)
{
if (entry.Utc > highestTime && entry.Utc < moment)
{
highestTime = entry.Utc;
result = entry;
}
}
return result;
}
private BlockTimeEntry FindClosestAfterEntry(DateTime moment)
{
var result = entries.Values.First();
var lowestTime = result.Utc;
foreach (var entry in entries.Values)
{
if (entry.Utc < lowestTime && entry.Utc > moment)
{
lowestTime = entry.Utc;
result = entry;
}
}
return result;
}
}
}