Skip to content

Commit 4b497d9

Browse files
authored
Add GetBlocks and CommonBlock APIs for chain synchronisation (stratisproject#985)
* Add GetBlocks and CommonBlock APIs for chain synchronisation * Fix typo * Change method * Add tests
1 parent d99b00e commit 4b497d9

File tree

5 files changed

+180
-18
lines changed

5 files changed

+180
-18
lines changed

src/Stratis.Bitcoin.Features.BlockStore.Tests/BlockStoreControllerTests.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.ComponentModel.DataAnnotations;
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Linq;
24
using System.Net;
35
using FluentAssertions;
46
using Microsoft.AspNetCore.Mvc;
@@ -171,6 +173,34 @@ public void GetBlockCount_ReturnsHeightFromChainState()
171173
Assert.Equal(2, result);
172174
}
173175

176+
[Fact]
177+
public void Get_Blocks_Should_Return_Blocks()
178+
{
179+
var logger = new Mock<ILoggerFactory>();
180+
var store = new Mock<IBlockStore>();
181+
var chainState = new Mock<IChainState>();
182+
var addressIndexer = new Mock<IAddressIndexer>();
183+
var utxoIndexer = new Mock<IUtxoIndexer>();
184+
var scriptAddressReader = Mock.Of<IScriptAddressReader>();
185+
186+
ChainIndexer chainIndexer = WalletTestsHelpers.GenerateChainWithHeight(3, new StraxTest());
187+
188+
logger.Setup(l => l.CreateLogger(It.IsAny<string>())).Returns(Mock.Of<ILogger>);
189+
190+
chainState.Setup(c => c.ConsensusTip)
191+
.Returns(chainIndexer.GetHeader(2));
192+
193+
store.Setup(s => s.GetBlocks(It.IsAny<List<uint256>>())).Returns((List<uint256> hashes) => hashes.Select(h => chainIndexer[h].Block).ToList());
194+
195+
var controller = new BlockStoreController(new StraxMain(), logger.Object, store.Object, chainState.Object, chainIndexer, addressIndexer.Object, utxoIndexer.Object, scriptAddressReader);
196+
197+
var json = (JsonResult)controller.GetBlocks(new SearchByHeightRequest() { Height = 2, NumberOfBlocks = 2 });
198+
var jsonResult = Assert.IsType<List<Block>>(json.Value);
199+
200+
Assert.Equal(2, jsonResult.Count);
201+
202+
}
203+
174204
private static (Mock<IBlockStore> store, BlockStoreController controller) GetControllerAndStore()
175205
{
176206
var logger = new Mock<ILoggerFactory>();

src/Stratis.Bitcoin.Features.BlockStore/Controllers/BlockStoreController.cs

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public static class BlockStoreRouteEndPoint
2525
public const string GetVerboseAddressesBalances = "getverboseaddressesbalances";
2626
public const string GetAddressIndexerTip = "addressindexertip";
2727
public const string GetBlock = "block";
28+
public const string GetBlocks = "blocks";
2829
public const string GetBlockCount = "getblockcount";
2930
public const string GetUtxoSet = "getutxoset";
3031
public const string GetUtxoSetForAddress = "getutxosetforaddress";
@@ -149,29 +150,55 @@ public IActionResult GetBlock([FromQuery] SearchByHashRequest query)
149150
return this.Json(block);
150151
}
151152

152-
BlockModel blockModel = query.ShowTransactionDetails
153-
? new BlockTransactionDetailsModel(block, chainedHeader, this.chainIndexer.Tip, this.network)
154-
: new BlockModel(block, chainedHeader, this.chainIndexer.Tip, this.network);
153+
return this.Json(GetBlockModel(block, blockId, chainedHeader, query.ShowTransactionDetails));
154+
}
155+
catch (Exception e)
156+
{
157+
this.logger.LogError("Exception occurred: {0}", e.ToString());
158+
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
159+
}
160+
}
155161

156-
if (this.network.Consensus.IsProofOfStake)
157-
{
158-
var posBlock = block as PosBlock;
159162

160-
blockModel.PosBlockSignature = posBlock.BlockSignature.ToHex(this.network);
161-
blockModel.PosBlockTrust = new Target(chainedHeader.GetBlockTarget()).ToUInt256().ToString();
162-
blockModel.PosChainTrust = chainedHeader.ChainWork.ToString(); // this should be similar to ChainWork
163+
/// <summary>
164+
/// Retrieves the blocks from a given height onwards.
165+
/// </summary>
166+
/// <param name="query">An object containing the necessary parameters to search for a block.</param>
167+
/// <returns><see cref="BlockModel"/> if block is found, <see cref="NotFoundObjectResult"/> if not found. Returns <see cref="IActionResult"/> with error information if exception thrown.</returns>
168+
/// <response code="200">Returns data about the block or block not found message</response>
169+
/// <response code="400">Block hash invalid, or an unexpected exception occurred</response>
170+
[Route(BlockStoreRouteEndPoint.GetBlocks)]
171+
[HttpGet]
172+
[ProducesResponseType((int)HttpStatusCode.OK)]
173+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
174+
[ProducesResponseType((int)HttpStatusCode.NotFound)]
175+
public IActionResult GetBlocks([FromQuery] SearchByHeightRequest query)
176+
{
177+
if (!this.ModelState.IsValid)
178+
return ModelStateErrors.BuildErrorResponse(this.ModelState);
179+
180+
try
181+
{
182+
if (query.Height > this.chainIndexer.Tip.Height)
183+
return this.NotFound("No blocks found");
163184

164-
if (this.stakeChain != null)
165-
{
166-
BlockStake blockStake = this.stakeChain.Get(blockId);
185+
int tip = query.Height + query.NumberOfBlocks - 1;
186+
if (tip > this.chainIndexer.Tip.Height)
187+
{
188+
query.NumberOfBlocks -= (this.chainIndexer.Tip.Height - tip);
189+
tip = this.chainIndexer.Tip.Height;
190+
}
167191

168-
blockModel.PosModifierv2 = blockStake?.StakeModifierV2.ToString();
169-
blockModel.PosFlags = blockStake?.Flags == BlockFlag.BLOCK_PROOF_OF_STAKE ? "proof-of-stake" : "proof-of-work";
170-
blockModel.PosHashProof = blockStake?.HashProof?.ToString();
171-
}
192+
List<ChainedHeader> chainedHeaders = this.chainIndexer[tip].EnumerateToGenesis().Take(query.NumberOfBlocks).Reverse().ToList();
193+
List<uint256> blockIds = chainedHeaders.Select(h => h.HashBlock).ToList();
194+
List<Block> blocks = this.blockStore.GetBlocks(blockIds);
195+
196+
if (!query.OutputJson)
197+
{
198+
return this.Json(blocks);
172199
}
173200

174-
return this.Json(blockModel);
201+
return this.Json(chainedHeaders.Select((h, n) => GetBlockModel(blocks[n], blockIds[n], h, query.ShowTransactionDetails)));
175202
}
176203
catch (Exception e)
177204
{
@@ -180,6 +207,33 @@ public IActionResult GetBlock([FromQuery] SearchByHashRequest query)
180207
}
181208
}
182209

210+
private BlockModel GetBlockModel(Block block, uint256 blockId, ChainedHeader chainedHeader, bool showTransactionDetails)
211+
{
212+
BlockModel blockModel = showTransactionDetails
213+
? new BlockTransactionDetailsModel(block, chainedHeader, this.chainIndexer.Tip, this.network)
214+
: new BlockModel(block, chainedHeader, this.chainIndexer.Tip, this.network);
215+
216+
if (this.network.Consensus.IsProofOfStake)
217+
{
218+
var posBlock = block as PosBlock;
219+
220+
blockModel.PosBlockSignature = posBlock.BlockSignature.ToHex(this.network);
221+
blockModel.PosBlockTrust = new Target(chainedHeader.GetBlockTarget()).ToUInt256().ToString();
222+
blockModel.PosChainTrust = chainedHeader.ChainWork.ToString(); // this should be similar to ChainWork
223+
224+
if (this.stakeChain != null)
225+
{
226+
BlockStake blockStake = this.stakeChain.Get(blockId);
227+
228+
blockModel.PosModifierv2 = blockStake?.StakeModifierV2.ToString();
229+
blockModel.PosFlags = blockStake?.Flags == BlockFlag.BLOCK_PROOF_OF_STAKE ? "proof-of-stake" : "proof-of-work";
230+
blockModel.PosHashProof = blockStake?.HashProof?.ToString();
231+
}
232+
}
233+
234+
return blockModel;
235+
}
236+
183237
/// <summary>
184238
/// Gets the current consensus tip height.
185239
/// </summary>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Stratis.Bitcoin.Features.BlockStore.Models
4+
{
5+
/// <summary>
6+
/// A class containing the necessary parameters for a block search request.
7+
/// </summary>
8+
public class SearchByHeightRequest : RequestBase
9+
{
10+
/// <summary>
11+
/// The height of the required block(s).
12+
/// </summary>
13+
[Required]
14+
public int Height { get; set; }
15+
16+
/// <summary>
17+
/// The maximum number of the blocks to return.
18+
/// </summary>
19+
[Required]
20+
public int NumberOfBlocks { get; set; }
21+
22+
/// <summary>
23+
/// A flag that indicates whether to return each block transaction complete with details
24+
/// or simply return transaction hashes (TX IDs).
25+
/// </summary>
26+
/// <remarks>This flag is not used when <see cref="RequestBase.OutputJson"/> is set to false.</remarks>
27+
public bool ShowTransactionDetails { get; set; }
28+
}
29+
}

src/Stratis.Bitcoin.Features.Consensus/ConsensusController.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,32 @@ public uint256 GetBestBlockHashRPC()
5050
return this.ChainState.ConsensusTip?.HashBlock;
5151
}
5252

53+
/// <summary>
54+
/// Finds the first block in common with the list of provided hashes. Include the genesis hash.
55+
/// </summary>
56+
/// <param name="blockLocator">The list of provided hashes.</param>
57+
/// <returns>A <see cref="JsonResult"/> derived from a <see cref="HashHeightPair"/> object.</returns>
58+
[Route("api/[controller]/commonblock")]
59+
[HttpPost]
60+
[ProducesResponseType((int)HttpStatusCode.OK)]
61+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
62+
public IActionResult CommonBlock([FromBody] uint256[] blockLocator)
63+
{
64+
try
65+
{
66+
ChainedHeader commonHeader = this.ChainIndexer.FindFork(blockLocator);
67+
if (commonHeader == null)
68+
throw new BlockNotFoundException($"No common block found");
69+
70+
return this.Json(new HashHeightPair(commonHeader));
71+
}
72+
catch (Exception e)
73+
{
74+
this.logger.LogError("Exception occurred: {0}", e.ToString());
75+
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
76+
}
77+
}
78+
5379
/// <summary>
5480
/// Get the threshold states of softforks currently being deployed.
5581
/// Allowable states are: Defined, Started, LockedIn, Failed, Active.

src/Stratis.Bitcoin.IntegrationTests/RPC/ConsensusActionTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
using Xunit;
66
using Stratis.Bitcoin.Base;
77
using Stratis.Bitcoin.Tests.Common;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Stratis.Bitcoin.Utilities.JsonErrors;
10+
using Stratis.Bitcoin.Utilities;
811

912
namespace Stratis.Bitcoin.IntegrationTests.RPC
1013
{
@@ -49,5 +52,25 @@ public void CanCall_IsInitialBlockDownload()
4952
Assert.NotNull(isIBDProvider);
5053
Assert.True(isIBDProvider.IsInitialBlockDownload());
5154
}
55+
56+
[Fact]
57+
public void CanCall_CommonBlock()
58+
{
59+
string dir = CreateTestDir(this);
60+
61+
IFullNode fullNode = this.BuildServicedNode(dir);
62+
var controller = fullNode.NodeController<ConsensusController>();
63+
64+
IActionResult result = controller.CommonBlock(new[] { uint256.Zero, fullNode.Network.GenesisHash, uint256.One });
65+
66+
Assert.NotNull(result);
67+
68+
var jsonResult = Assert.IsType<JsonResult>(result);
69+
70+
var hashHeightPair = Assert.IsType<HashHeightPair>(jsonResult.Value);
71+
72+
Assert.Equal(hashHeightPair.Height, 0);
73+
Assert.Equal(hashHeightPair.Hash, fullNode.Network.GenesisHash);
74+
}
5275
}
5376
}

0 commit comments

Comments
 (0)