Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bulk fetching for blocks by headerIds #2043

Merged
merged 3 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 36 additions & 0 deletions src/main/resources/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2721,6 +2721,42 @@ paths:
schema:
$ref: '#/components/schemas/ApiError'

/blocks/headerIdsList:
ccellado marked this conversation as resolved.
Show resolved Hide resolved
get:
summary: Get the list of full block info by given header ids
operationId: getFullBlockByIdsList
tags:
- blocks
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: string
responses:
'200':
description: List of block objects representing the full block data
ccellado marked this conversation as resolved.
Show resolved Hide resolved
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/FullBlock'
'404':
description: Blocks with this ids doesn't exist
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'

/blocks/{headerId}/header:
get:
summary: Get the block header info by a given header id
Expand Down
10 changes: 10 additions & 0 deletions src/main/scala/org/ergoplatform/http/api/BlocksApiRoute.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ case class BlocksApiRoute(viewHolderRef: ActorRef, readersHolder: ActorRef, ergo
getChainSliceR ~
getBlockIdsAtHeightR ~
getBlockHeaderByHeaderIdR ~
getFullBlockByHeaderIdsListR ~
getBlockTransactionsByHeaderIdR ~
getProofForTxR ~
getFullBlockByHeaderIdR ~
Expand Down Expand Up @@ -69,6 +70,11 @@ case class BlocksApiRoute(viewHolderRef: ActorRef, readersHolder: ActorRef, ergo
history.typedModifierById[Header](headerId).flatMap(history.getFullBlock)
}

private def getFullBlockByHeaderIdsList(headerIds: Seq[ModifierId]): Future[Seq[ErgoFullBlock]] =
getHistory.map { history =>
headerIds.flatMap(headerId => history.typedModifierById[Header](headerId).flatMap(history.getFullBlock))
}

private def getModifierById(modifierId: ModifierId): Future[Option[BlockSection]] =
getHistory.map(_.modifierById(modifierId))

Expand Down Expand Up @@ -177,4 +183,8 @@ case class BlocksApiRoute(viewHolderRef: ActorRef, readersHolder: ActorRef, ergo
ApiResponse(getFullBlockByHeaderId(id))
}

def getFullBlockByHeaderIdsListR: Route = (post & path("headerIdsList") & modifierIds) { ids =>
ApiResponse(getFullBlockByHeaderIdsList(ids))
}

}
63 changes: 48 additions & 15 deletions src/main/scala/org/ergoplatform/http/api/ErgoBaseApiRoute.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.ergoplatform.nodeView.mempool.ErgoMemPoolReader
import org.ergoplatform.nodeView.state.{ErgoStateReader, UtxoStateReader}
import org.ergoplatform.settings.{Algos, ErgoSettings}
import scorex.core.api.http.{ApiError, ApiRoute}
import scorex.util.{ModifierId, bytesToId}
import scorex.util.{bytesToId, ModifierId}
import akka.pattern.ask
import io.circe.syntax.EncoderOps
import org.ergoplatform.nodeView.ErgoNodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction
Expand All @@ -20,21 +20,40 @@ import sigmastate.Values.ErgoTree
import sigmastate.serialization.ErgoTreeSerializer

import scala.concurrent.{ExecutionContextExecutor, Future}
import scala.util.{Success, Try}
import scala.util.{Failure, Success, Try}

trait ErgoBaseApiRoute extends ApiRoute with ApiCodecs {

implicit val ec: ExecutionContextExecutor = context.dispatcher

val modifierId: Directive1[ModifierId] = pathPrefix(Segment).flatMap(handleModifierId)

val modifierIds: Directive1[Seq[ModifierId]] =
entity(as[Seq[String]]).flatMap(handleModifierIds)

val modifierIdGet: Directive1[ModifierId] = parameters("id".as[String])
.flatMap(handleModifierId)

private def handleModifierId(value: String): Directive1[ModifierId] = {
Algos.decode(value) match {
case Success(bytes) => provide(bytesToId(bytes))
case _ => reject(ValidationRejection("Wrong modifierId format"))
case _ => reject(ValidationRejection("Wrong modifierId format"))
}
}

private def handleModifierIds(values: Seq[String]): Directive1[Seq[ModifierId]] = {
val acc = collection.mutable.Buffer.empty[ModifierId]
val err = collection.mutable.Buffer.empty[String]
for (value <- values) {
Algos.decode(value) match {
case Success(bytes) => acc += bytesToId(bytes)
case Failure(e) => err += e.getMessage
}
}
if (err.nonEmpty) {
reject(ValidationRejection(s"Wrong modifierId format for: ${err.mkString(",")}"))
} else {
provide(acc)
}
}

Expand All @@ -49,13 +68,16 @@ trait ErgoBaseApiRoute extends ApiRoute with ApiCodecs {

private def handleErgoTree(value: String): Directive1[ErgoTree] = {
Base16.decode(fromJsonOrPlain(value)) match {
case Success(bytes) => provide(ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(bytes))
case Success(bytes) =>
provide(ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(bytes))
case _ => reject(ValidationRejection("Invalid hex data"))
}

}

private def getStateAndPool(readersHolder: ActorRef): Future[(ErgoStateReader, ErgoMemPoolReader)] = {
private def getStateAndPool(
readersHolder: ActorRef
): Future[(ErgoStateReader, ErgoMemPoolReader)] = {
(readersHolder ? GetReaders).mapTo[Readers].map { rs =>
(rs.s, rs.m)
}
Expand All @@ -65,14 +87,18 @@ trait ErgoBaseApiRoute extends ApiRoute with ApiCodecs {
* Send local transaction to ErgoNodeViewHolder
* @return Transaction Id with status OK(200), or BadRequest(400)
*/
protected def sendLocalTransactionRoute(nodeViewActorRef: ActorRef, unconfirmedTx: UnconfirmedTransaction): Route = {
protected def sendLocalTransactionRoute(
nodeViewActorRef: ActorRef,
unconfirmedTx: UnconfirmedTransaction
): Route = {
val resultFuture =
(nodeViewActorRef ? LocallyGeneratedTransaction(unconfirmedTx))
.mapTo[ProcessingOutcome]
.flatMap {
case _: Accepted => Future.successful(unconfirmedTx.transaction.id)
case _: DoubleSpendingLoser => Future.failed(new IllegalArgumentException("Double spending attempt"))
case d: Declined => Future.failed(d.e)
case _: DoubleSpendingLoser =>
Future.failed(new IllegalArgumentException("Double spending attempt"))
case d: Declined => Future.failed(d.e)
case i: Invalidated => Future.failed(i.e)
}
completeOrRecoverWith(resultFuture) { ex =>
Expand All @@ -87,21 +113,28 @@ trait ErgoBaseApiRoute extends ApiRoute with ApiCodecs {
* Used in /transactions (POST /transactions and /transactions/check methods) and /wallet (/wallet/payment/send
* and /wallet/transaction/send) API methods to check submitted or generated transaction
*/
protected def verifyTransaction(tx: ErgoTransaction,
readersHolder: ActorRef,
ergoSettings: ErgoSettings): Future[Try[UnconfirmedTransaction]] = {
protected def verifyTransaction(
tx: ErgoTransaction,
readersHolder: ActorRef,
ergoSettings: ErgoSettings
): Future[Try[UnconfirmedTransaction]] = {
val now: Long = System.currentTimeMillis()
val bytes = Some(tx.bytes)
val bytes = Some(tx.bytes)

getStateAndPool(readersHolder)
.map {
case (utxo: UtxoStateReader, mp: ErgoMemPoolReader) =>
val maxTxCost = ergoSettings.nodeSettings.maxTransactionCost
utxo.withMempool(mp)
utxo
.withMempool(mp)
.validateWithCost(tx, maxTxCost)
.map(cost => new UnconfirmedTransaction(tx, Some(cost), now, now, bytes, source = None))
.map(cost =>
new UnconfirmedTransaction(tx, Some(cost), now, now, bytes, source = None)
)
case _ =>
tx.statelessValidity().map(_ => new UnconfirmedTransaction(tx, None, now, now, bytes, source = None))
tx.statelessValidity()
.map(_ => new UnconfirmedTransaction(tx, None, now, now, bytes, source = None)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.ergoplatform.http.routes
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes, UniversalEntity}
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.ScalatestRouteTest
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import io.circe.syntax._
import org.ergoplatform.http.api.BlocksApiRoute
import org.ergoplatform.modifiers.ErgoFullBlock
Expand All @@ -13,29 +14,36 @@ import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import scorex.util.ModifierId

class BlocksApiRouteSpec extends AnyFlatSpec
class BlocksApiRouteSpec
extends AnyFlatSpec
with Matchers
with ScalatestRouteTest
with FailFastCirceSupport
with Stubs {

val prefix = "/blocks"

val route: Route = BlocksApiRoute(nodeViewRef, digestReadersRef, settings).route

val headerIdBytes: ModifierId = history.lastHeaders(1).headers.head.id
val headerIdString: String = Algos.encode(headerIdBytes)
val headerIdString: String = Algos.encode(headerIdBytes)

it should "get last blocks" in {
Get(prefix) ~> route ~> check {
status shouldBe StatusCodes.OK
history.headerIdsAt(0, 50).map(Algos.encode).asJson.toString() shouldEqual responseAs[String]
history
.headerIdsAt(0, 50)
.map(Algos.encode)
.asJson
.toString() shouldEqual responseAs[String]
}
}

it should "post block correctly" in {
val (st, bh) = createUtxoState(settings)
val (st, bh) = createUtxoState(settings)
val block: ErgoFullBlock = validFullBlock(parentOpt = None, st, bh)
val blockJson: UniversalEntity = HttpEntity(block.asJson.toString).withContentType(ContentTypes.`application/json`)
val blockJson: UniversalEntity =
HttpEntity(block.asJson.toString).withContentType(ContentTypes.`application/json`)
Post(prefix, blockJson) ~> route ~> check {
status shouldBe StatusCodes.OK
}
Expand All @@ -44,14 +52,23 @@ class BlocksApiRouteSpec extends AnyFlatSpec
it should "get last headers" in {
Get(prefix + "/lastHeaders/1") ~> route ~> check {
status shouldBe StatusCodes.OK
history.lastHeaders(1).headers.map(_.asJson).asJson.toString() shouldEqual responseAs[String]
history
.lastHeaders(1)
.headers
.map(_.asJson)
.asJson
.toString() shouldEqual responseAs[String]
}
}

it should "get block at height" in {
Get(prefix + "/at/0") ~> route ~> check {
status shouldBe StatusCodes.OK
history.headerIdsAtHeight(0).map(Algos.encode).asJson.toString() shouldEqual responseAs[String]
history
.headerIdsAtHeight(0)
.map(Algos.encode)
.asJson
.toString() shouldEqual responseAs[String]
}
}

Expand All @@ -69,7 +86,8 @@ class BlocksApiRouteSpec extends AnyFlatSpec
it should "get block by header id" in {
Get(prefix + "/" + headerIdString) ~> route ~> check {
status shouldBe StatusCodes.OK
val expected = history.typedModifierById[Header](headerIdBytes)
val expected = history
.typedModifierById[Header](headerIdBytes)
.flatMap(history.getFullBlock)
.map(_.asJson)
.get
Expand All @@ -78,6 +96,23 @@ class BlocksApiRouteSpec extends AnyFlatSpec
}
}

it should "get blocks by header ids" in {
val headerIdsBytes = history.lastHeaders(10).headers
val headerIdsString: Seq[String] = headerIdsBytes.map(h => Algos.encode(h.id))

Post(prefix + "/headerIdsList", headerIdsString.asJson) ~> route ~> check {
status shouldBe StatusCodes.OK

val expected = headerIdsBytes
.map(_.id)
.flatMap(headerId =>
history.typedModifierById[Header](headerId).flatMap(history.getFullBlock)
)

responseAs[Seq[ErgoFullBlock]] shouldEqual expected
}
}

it should "get header by header id" in {
Get(prefix + "/" + headerIdString + "/header") ~> route ~> check {
status shouldBe StatusCodes.OK
Expand All @@ -94,9 +129,9 @@ class BlocksApiRouteSpec extends AnyFlatSpec
it should "get transactions by header id" in {
Get(prefix + "/" + headerIdString + "/transactions") ~> route ~> check {
status shouldBe StatusCodes.OK
val header = history.typedModifierById[Header](headerIdBytes).value
val header = history.typedModifierById[Header](headerIdBytes).value
val fullBlock = history.getFullBlock(header).value
val expected = fullBlock.blockTransactions.asJson.toString
val expected = fullBlock.blockTransactions.asJson.toString
responseAs[String] shouldEqual expected
}
}
Expand Down
Loading