Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Features.PoA;
using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Utilities.JsonErrors;
Expand All @@ -24,6 +25,7 @@ public static class FederationGatewayRouteEndPoint

public const string GetMaturedBlockDeposits = "get_matured_block_deposits";
public const string GetInfo = "info";
public const string GetBlockHeightClosestToTimestamp = "get_block_height_closest_to_timestamp";
}

/// <summary>
Expand All @@ -49,6 +51,8 @@ public class FederationGatewayController : Controller

private readonly FederationManager federationManager;

private readonly IConsensusManager consensusManager;

public FederationGatewayController(
ILoggerFactory loggerFactory,
Network network,
Expand All @@ -57,6 +61,7 @@ public FederationGatewayController(
ILeaderReceiver leaderReceiver,
IFederationGatewaySettings federationGatewaySettings,
IFederationWalletManager federationWalletManager,
IConsensusManager consensusManager,
FederationManager federationManager = null)
{
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
Expand All @@ -67,6 +72,7 @@ public FederationGatewayController(
this.federationGatewaySettings = federationGatewaySettings;
this.federationWalletManager = federationWalletManager;
this.federationManager = federationManager;
this.consensusManager = consensusManager;
}

/// <summary>Pushes the current block tip to be used for updating the federated leader in a round robin fashion.</summary>
Expand Down Expand Up @@ -165,6 +171,39 @@ public IActionResult GetInfo()
}
}

/// <summary>Finds first block with timestamp lower than <paramref name="timestamp"/> and return's it's height.</summary>
/// <returns><see cref="ClosestHeightModel"/> model with height of the block.</returns>
[Route(FederationGatewayRouteEndPoint.GetBlockHeightClosestToTimestamp)]
[HttpGet]
public JsonResult GetBlockHeightClosestToTimestamp(uint timestamp)
{
try
{
ChainedHeader current = this.consensusManager.Tip;

while (current.Height != 0)
{
if (current.Header.Time <= timestamp)
{
var model = new ClosestHeightModel() { Height = current.Height };

return this.Json(model);
}

current = current.Previous;
}

return this.Json(new ClosestHeightModel() { Height = 0 }); ;
}
catch (Exception e)
{
this.logger.LogTrace("Exception thrown calling /api/FederationGateway/{0}: {1}.", FederationGatewayRouteEndPoint.GetBlockHeightClosestToTimestamp, e.Message);
ErrorResult errorResult = ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());

return this.Json(errorResult);
}
}

/// <summary>
/// Builds an <see cref="IActionResult"/> containing errors contained in the <see cref="ControllerBase.ModelState"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Stratis.FederatedPeg.Features.FederationGateway.Models
{
public class ClosestHeightModel
{
[JsonProperty(PropertyName = "height")]
public int Height { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public interface IFederationGatewayClient

/// <summary><see cref="FederationGatewayController.GetMaturedBlockDepositsAsync"/></summary>
Task<List<MaturedBlockDepositsModel>> GetMaturedBlockDepositsAsync(MaturedBlockRequestModel model, CancellationToken cancellation = default(CancellationToken));

/// <summary><see cref="FederationGatewayController.GetBlockHeightClosestToTimestamp"/></summary>
Task<ClosestHeightModel> GetBlockHeightClosestToTimestampAsync(uint timestamp, CancellationToken cancellation = default(CancellationToken));
}

/// <inheritdoc cref="IFederationGatewayClient"/>
Expand All @@ -37,5 +40,13 @@ public FederationGatewayClient(ILoggerFactory loggerFactory, IFederationGatewayS
{
return this.SendPostRequestAsync<MaturedBlockRequestModel, List<MaturedBlockDepositsModel>>(model, FederationGatewayRouteEndPoint.GetMaturedBlockDeposits, cancellation);
}

/// <inheritdoc />
public Task<ClosestHeightModel> GetBlockHeightClosestToTimestampAsync(uint timestamp, CancellationToken cancellation = default(CancellationToken))
{
string parameters = $"{nameof(timestamp)}={timestamp}";

return this.SendGetRequestAsync<ClosestHeightModel>(FederationGatewayRouteEndPoint.GetBlockHeightClosestToTimestamp, cancellation, parameters);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,51 @@ public RestApiClientBase(ILoggerFactory loggerFactory, IFederationGatewaySetting
}, onRetry: OnRetry);
}

protected async Task<HttpResponseMessage> SendGetRequestAsync(string apiMethodName, CancellationToken cancellation, string requestParameters = null)
{
string url = $"{this.endpointUrl}/{apiMethodName}";

if (requestParameters != null)
url += $"?{requestParameters}";

var publicationUri = new Uri(url);

HttpResponseMessage response = null;

using (HttpClient client = this.httpClientFactory.CreateClient())
{
try
{
// Retry the following call according to the policy.
await this.policy.ExecuteAsync(async token =>
{
this.logger.LogDebug("Sending get request to Uri '{0}'.", publicationUri);
response = await client.GetAsync(publicationUri, cancellation).ConfigureAwait(false);
}, cancellation);
}
catch (OperationCanceledException)
{
this.logger.LogTrace("(-)[CANCELLED]:null");
return null;
}
catch (Exception ex)
{
this.logger.LogError("The counter-chain daemon is not ready to receive API calls at this time ({0})", publicationUri);
return new HttpResponseMessage() { ReasonPhrase = ex.Message, StatusCode = HttpStatusCode.InternalServerError };
}
}

this.logger.LogTrace("(-)[SUCCESS]");
return response;
}

protected async Task<Response> SendGetRequestAsync<Response>(string apiMethodName, CancellationToken cancellation, string requestParameters = null) where Response : class
{
HttpResponseMessage response = await this.SendGetRequestAsync(apiMethodName, cancellation, requestParameters).ConfigureAwait(false);

return await this.ParseResponseAsync<Response>(response).ConfigureAwait(false);
}

protected async Task<HttpResponseMessage> SendPostRequestAsync<Model>(Model requestModel, string apiMethodName, CancellationToken cancellation) where Model : class
{
var publicationUri = new Uri($"{this.endpointUrl}/{apiMethodName}");
Expand Down Expand Up @@ -93,10 +138,15 @@ protected async Task<Response> SendPostRequestAsync<Model, Response>(Model reque
{
HttpResponseMessage response = await this.SendPostRequestAsync(requestModel, apiMethodName, cancellation).ConfigureAwait(false);

return await this.ParseResponseAsync<Response>(response).ConfigureAwait(false);
}

private async Task<Response> ParseResponseAsync<Response>(HttpResponseMessage message) where Response : class
{
// Parse response.
if ((response != null) && response.IsSuccessStatusCode && (response.Content != null))
if ((message != null) && message.IsSuccessStatusCode && (message.Content != null))
{
string successJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
string successJson = await message.Content.ReadAsStringAsync().ConfigureAwait(false);
if (successJson != null)
{
Response responseModel = JsonConvert.DeserializeObject<Response>(successJson);
Expand Down
25 changes: 25 additions & 0 deletions src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Protocol;
using Newtonsoft.Json;
using NSubstitute;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Configuration;
Expand Down Expand Up @@ -74,6 +75,7 @@ private FederationGatewayController CreateController()
this.leaderReceiver,
this.federationGatewaySettings,
this.federationWalletManager,
this.consensusManager,
this.federationManager);

return controller;
Expand Down Expand Up @@ -236,6 +238,7 @@ public void Call_Sidechain_Gateway_Get_Info()
this.leaderReceiver,
settings,
this.federationWalletManager,
this.consensusManager,
this.federationManager);

IActionResult result = controller.GetInfo();
Expand Down Expand Up @@ -275,6 +278,7 @@ public void Call_Mainchain_Gateway_Get_Info()
this.leaderReceiver,
settings,
this.federationWalletManager,
this.consensusManager,
this.federationManager);

IActionResult result = controller.GetInfo();
Expand All @@ -293,5 +297,26 @@ public void Call_Mainchain_Gateway_Get_Info()
model.MinimumDepositConfirmations.Should().Be(1);
model.MultisigPublicKey.Should().Be(multisigPubKey);
}

[Fact]
public void GetBlockHeightClosestToTimestampTest()
{
FederationGatewayController controller = this.CreateController();

List<ChainedHeader> headers = ChainedHeadersHelper.CreateConsecutiveHeaders(1000);

for (int i = 0; i < headers.Count; i++)
headers[i].Header.Time = (uint)(i * 3);

this.consensusManager.Tip.Returns(headers.Last());

ChainedHeader targetHeader = headers[500];

JsonResult result = controller.GetBlockHeightClosestToTimestamp(targetHeader.Header.Time);

var responseHeight = result.Value as ClosestHeightModel;

Assert.Equal(targetHeader.Height, responseHeight.Height);
}
}
}