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

feat: add cl0 transaction tracking #987

Open
wants to merge 1 commit into
base: feature/cl0_transaction_tracking
Choose a base branch
from
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 @@ -134,7 +134,8 @@ abstract class CurrencyL0App(
tessellationVersion,
cfg.http,
services.dataApplication,
metagraphVersion.some
metagraphVersion.some,
sharedServices.currencySnapshotEventValidationErrorStorage
)
_ <- MkHttpServer[IO].newEmber(ServerName("public"), cfg.http.publicHttp, api.publicApp)
_ <- MkHttpServer[IO].newEmber(ServerName("p2p"), cfg.http.p2pHttp, api.p2pApp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ object method {
c.feeConfigs.get(environment).map(SortedMap.from(_)).getOrElse(SortedMap.empty),
c.forkInfoStorage,
c.lastKryoHashOrdinal,
c.addresses
c.addresses,
c.validationErrorStorage.getOrElse(ValidationErrorStorageConfig.default)
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.constellationnetwork.currency.l0.http.routes

import cats.effect.Async
import cats.syntax.flatMap._
import cats.syntax.functor._

import io.constellationnetwork.currency.dataApplication.dataApplication.DataApplicationBlock
import io.constellationnetwork.node.shared.domain.block.processing._
import io.constellationnetwork.node.shared.infrastructure.consensus.{ValidationErrorStorage, ValidationErrorStorageEntry}
import io.constellationnetwork.node.shared.snapshot.currency
import io.constellationnetwork.node.shared.snapshot.currency.CurrencySnapshotEvent
import io.constellationnetwork.routes.internal._
import io.constellationnetwork.security.hash.Hash

import derevo.circe.magnolia.encoder
import derevo.derive
import eu.timepit.refined.auto._
import io.circe.syntax.EncoderOps
import io.circe.{Encoder, Json}
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl

@derive(encoder)
case class ValidationError(
message: String,
description: Option[String] = None,
details: Option[BlockRejectionReason] = None
)

final case class ValidationErrorRoutes[F[_]: Async](
currencySnapshotValidationErrorStorage: ValidationErrorStorage[F, CurrencySnapshotEvent, BlockRejectionReason]
) extends Http4sDsl[F]
with PublicRoutes[F] {

implicit val dataApplicationBlockEncoder: Encoder[DataApplicationBlock] = new Encoder[DataApplicationBlock] {
def apply(a: DataApplicationBlock): Json =
Json.obj(
"roundId" -> a.roundId.asJson,
"updatesHashes" -> a.updatesHashes.asJson
)
}

private def blockRejectionReasonToValidationError(a: BlockRejectionReason): ValidationError = {
val message = a match {
case e: DataBlockNotAccepted => s"data block not accepted: ${e.reason}"
case InvalidCurrencyMessageEvent => "invalid currency message"
case e: ParentNotFound => s"parent not found: ${e.parent}"
case e: RejectedTransaction => s"rejected transaction: ${e.reason} tx=${e.tx}"
case SnapshotOrdinalUnavailable => "snapshot ordinal unavailable"
case e: ValidationFailed => s"validation failed: ${e.reasons}"
}
ValidationError(
message = message,
details = Some(a)
)
}

implicit val encoder: Encoder[ValidationErrorStorageEntry[CurrencySnapshotEvent, BlockRejectionReason]] =
new Encoder[ValidationErrorStorageEntry[CurrencySnapshotEvent, BlockRejectionReason]] {
def apply(a: ValidationErrorStorageEntry[CurrencySnapshotEvent, BlockRejectionReason]): Json = Json.obj(
(
"block",
a.artifact match {
case currency.BlockEvent(block) => block.asJson
case currency.DataApplicationBlockEvent(dataBlock) => dataBlock.asJson
case currency.CurrencyMessageEvent(message) => message.asJson
}
),
("reason", blockRejectionReasonToValidationError(a.validationError).asJson),
("timestamp", a.timestamp.asJson)
)
}

protected val prefixPath: InternalUrlPrefix = "/currency"

protected val public: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "events" / "errors" =>
currencySnapshotValidationErrorStorage.getAll.flatMap(Ok(_))
case GET -> Root / "transactions" / txHash / "errors" =>
currencySnapshotValidationErrorStorage
.get(Hash(txHash))
.map(_.map(blockRejectionReasonToValidationError))
.flatMap(Ok(_))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import io.constellationnetwork.currency.dataApplication.dataApplication.DataAppl
import io.constellationnetwork.currency.dataApplication.{BaseDataApplicationL0Service, L0NodeContext}
import io.constellationnetwork.currency.l0.cell.{L0Cell, L0CellInput}
import io.constellationnetwork.currency.l0.cli.method.Run
import io.constellationnetwork.currency.l0.http.routes.{CurrencyBlockRoutes, CurrencyMessageRoutes, DataBlockRoutes}
import io.constellationnetwork.currency.l0.http.routes._
import io.constellationnetwork.currency.l0.snapshot.CurrencySnapshotKey
import io.constellationnetwork.currency.l0.snapshot.schema.CurrencyConsensusOutcome
import io.constellationnetwork.currency.schema.currency._
import io.constellationnetwork.env.AppEnvironment
import io.constellationnetwork.env.AppEnvironment.{Dev, Integrationnet, Testnet}
import io.constellationnetwork.node.shared.config.types.HttpConfig
import io.constellationnetwork.node.shared.domain.block.processing.BlockRejectionReason
import io.constellationnetwork.node.shared.http.p2p.middlewares.{PeerAuthMiddleware, `X-Id-Middleware`}
import io.constellationnetwork.node.shared.http.routes._
import io.constellationnetwork.node.shared.infrastructure.consensus.ValidationErrorStorage
import io.constellationnetwork.node.shared.infrastructure.metrics.Metrics
import io.constellationnetwork.node.shared.snapshot.currency.CurrencySnapshotEvent
import io.constellationnetwork.schema.peer.PeerId
Expand All @@ -43,7 +45,8 @@ object HttpApi {
nodeVersion: TessellationVersion,
httpCfg: HttpConfig,
maybeDataApplication: Option[BaseDataApplicationL0Service[F]],
maybeMetagraphVersion: Option[MetagraphVersion]
maybeMetagraphVersion: Option[MetagraphVersion],
currencySnapshotEventValidationErrorStorage: ValidationErrorStorage[F, CurrencySnapshotEvent, BlockRejectionReason]
): HttpApi[F] =
new HttpApi[F](
validators,
Expand All @@ -57,7 +60,8 @@ object HttpApi {
nodeVersion,
httpCfg,
maybeDataApplication,
maybeMetagraphVersion
maybeMetagraphVersion,
currencySnapshotEventValidationErrorStorage
) {}
}

Expand All @@ -73,7 +77,8 @@ sealed abstract class HttpApi[F[_]: Async: SecurityProvider: HasherSelector: Met
nodeVersion: TessellationVersion,
httpCfg: HttpConfig,
maybeDataApplication: Option[BaseDataApplicationL0Service[F]],
maybeMetagraphVersion: Option[MetagraphVersion]
maybeMetagraphVersion: Option[MetagraphVersion],
currencySnapshotEventValidationErrorStorage: ValidationErrorStorage[F, CurrencySnapshotEvent, BlockRejectionReason]
) {

private val mkCell = (event: CurrencySnapshotEvent) =>
Expand All @@ -99,6 +104,7 @@ sealed abstract class HttpApi[F[_]: Async: SecurityProvider: HasherSelector: Met
implicit val (d, e) = (da.dataDecoder, da.calculatedStateEncoder)
DataBlockRoutes[F](mkCell, da)
}
private val validationErrorRoutes = ValidationErrorRoutes(currencySnapshotEventValidationErrorStorage)
private val metagraphNodeRoutes = maybeMetagraphVersion.map { metagraphVersion =>
MetagraphRoutes[F](storages.node, storages.session, storages.cluster, httpCfg, selfId, nodeVersion, metagraphVersion)
}
Expand Down Expand Up @@ -143,6 +149,7 @@ sealed abstract class HttpApi[F[_]: Async: SecurityProvider: HasherSelector: Met
clusterRoutes.publicRoutes <+>
currencyBlockRoutes.publicRoutes <+>
dataBlockRoutes.map(_.publicRoutes).getOrElse(HttpRoutes.empty) <+>
validationErrorRoutes.publicRoutes <+>
walletRoutes.publicRoutes <+>
nodeRoutes.publicRoutes <+>
consensusInfoRoutes.publicRoutes <+>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ object Services {
sharedServices.currencySnapshotAcceptanceManager,
dataApplicationAcceptanceManager,
cfg.snapshotSize,
sharedServices.currencyEventsCutter
sharedServices.currencyEventsCutter,
sharedServices.currencySnapshotEventValidationErrorStorage
)

validator = CurrencySnapshotValidator.make[F](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ object method {
c.feeConfigs.get(environment).map(SortedMap.from(_)).getOrElse(SortedMap.empty),
c.forkInfoStorage,
c.lastKryoHashOrdinal,
c.addresses
c.addresses,
c.validationErrorStorage.getOrElse(ValidationErrorStorageConfig.default)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import io.constellationnetwork.ext.cats.effect.ResourceIO
import io.constellationnetwork.json.{JsonBrotliBinarySerializer, JsonSerializer}
import io.constellationnetwork.kryo.KryoSerializer
import io.constellationnetwork.node.shared.config.types.{AddressesConfig, SnapshotSizeConfig}
import io.constellationnetwork.node.shared.domain.block.processing.BlockRejectionReason
import io.constellationnetwork.node.shared.domain.statechannel._
import io.constellationnetwork.node.shared.infrastructure.block.processing.BlockAcceptanceManager
import io.constellationnetwork.node.shared.infrastructure.consensus.ValidationErrorStorage
import io.constellationnetwork.node.shared.infrastructure.snapshot._
import io.constellationnetwork.node.shared.modules.SharedValidators
import io.constellationnetwork.node.shared.snapshot.currency.CurrencySnapshotEvent
import io.constellationnetwork.schema.address.Address
import io.constellationnetwork.schema.balance.Amount
import io.constellationnetwork.schema.peer.PeerId
Expand Down Expand Up @@ -75,8 +78,15 @@ object GlobalSnapshotStateChannelEventsProcessorSuite extends MutableIOSuite {
validators.currencyMessageValidator
)
currencyEventsCutter = CurrencyEventsCutter.make[IO](None)
validationErrorStorage <- ValidationErrorStorage.make[IO, CurrencySnapshotEvent, BlockRejectionReason](16, _ => IO.pure(List.empty))
creator = CurrencySnapshotCreator
.make[IO](currencySnapshotAcceptanceManager, None, SnapshotSizeConfig(Long.MaxValue, Long.MaxValue), currencyEventsCutter)
.make[IO](
currencySnapshotAcceptanceManager,
None,
SnapshotSizeConfig(Long.MaxValue, Long.MaxValue),
currencyEventsCutter,
validationErrorStorage
)
currencySnapshotValidator = CurrencySnapshotValidator.make[IO](creator, validators.signedValidator, None, None)
currencySnapshotContextFns = CurrencySnapshotContextFunctions.make(currencySnapshotValidator)
manager = new GlobalSnapshotStateChannelAcceptanceManager[IO] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ import cats.syntax.traverse._

import scala.collection.immutable.{SortedMap, SortedSet}

import io.constellationnetwork.dag.l0.infrastructure.snapshot._
import io.constellationnetwork.ext.cats.effect.ResourceIO
import io.constellationnetwork.ext.cats.syntax.next.catsSyntaxNext
import io.constellationnetwork.json.{JsonBrotliBinarySerializer, JsonSerializer}
import io.constellationnetwork.kryo.KryoSerializer
import io.constellationnetwork.node.shared.config.types.{AddressesConfig, SnapshotSizeConfig}
import io.constellationnetwork.node.shared.domain.block.processing.BlockRejectionReason
import io.constellationnetwork.node.shared.domain.statechannel.FeeCalculator
import io.constellationnetwork.node.shared.domain.transaction.{TransactionChainValidator, TransactionValidator}
import io.constellationnetwork.node.shared.infrastructure.block.processing.{BlockAcceptanceLogic, BlockAcceptanceManager, BlockValidator}
import io.constellationnetwork.node.shared.infrastructure.consensus.ValidationErrorStorage
import io.constellationnetwork.node.shared.infrastructure.metrics.Metrics
import io.constellationnetwork.node.shared.infrastructure.snapshot._
import io.constellationnetwork.node.shared.modules.SharedValidators
import io.constellationnetwork.node.shared.snapshot.currency.CurrencySnapshotEvent
import io.constellationnetwork.schema._
import io.constellationnetwork.schema.address.Address
import io.constellationnetwork.schema.balance.{Amount, Balance}
Expand Down Expand Up @@ -246,13 +248,20 @@ object GlobalSnapshotTraverseSuite extends MutableIOSuite with Checkers {

implicit val hs = HasherSelector.forSyncAlwaysCurrent(H)

val currencySnapshotCreator =
CurrencySnapshotCreator
.make[IO](currencySnapshotAcceptanceManager, None, SnapshotSizeConfig(Long.MaxValue, Long.MaxValue), currencyEventsCutter)
val currencySnapshotValidator = CurrencySnapshotValidator.make[IO](currencySnapshotCreator, validators.signedValidator, None, None)

val currencySnapshotContextFns = CurrencySnapshotContextFunctions.make(currencySnapshotValidator)
for {
validationErrorStorage <- ValidationErrorStorage.make[IO, CurrencySnapshotEvent, BlockRejectionReason](16, _ => IO.pure(List.empty))
currencySnapshotCreator =
CurrencySnapshotCreator
.make[IO](
currencySnapshotAcceptanceManager,
None,
SnapshotSizeConfig(Long.MaxValue, Long.MaxValue),
currencyEventsCutter,
validationErrorStorage
)
currencySnapshotValidator = CurrencySnapshotValidator.make[IO](currencySnapshotCreator, validators.signedValidator, None, None)

currencySnapshotContextFns = CurrencySnapshotContextFunctions.make(currencySnapshotValidator)
stateChannelManager <- GlobalSnapshotStateChannelAcceptanceManager.make[IO](None, NonNegLong(10L))
jsonBrotliBinarySerializer <- JsonBrotliBinarySerializer.forSync
feeCalculator = FeeCalculator.make(SortedMap.empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ import io.constellationnetwork.ext.collection.MapRefUtils._
import io.constellationnetwork.json.{JsonBrotliBinarySerializer, JsonSerializer}
import io.constellationnetwork.kryo.KryoSerializer
import io.constellationnetwork.node.shared.config.types.{AddressesConfig, SnapshotSizeConfig}
import io.constellationnetwork.node.shared.domain.block.processing.BlockRejectionReason
import io.constellationnetwork.node.shared.domain.statechannel.FeeCalculator
import io.constellationnetwork.node.shared.infrastructure.block.processing.BlockAcceptanceManager
import io.constellationnetwork.node.shared.infrastructure.consensus.ValidationErrorStorage
import io.constellationnetwork.node.shared.infrastructure.snapshot._
import io.constellationnetwork.node.shared.infrastructure.snapshot.storage.LastSnapshotStorage
import io.constellationnetwork.node.shared.modules.SharedValidators
import io.constellationnetwork.node.shared.nodeSharedKryoRegistrar
import io.constellationnetwork.node.shared.snapshot.currency.CurrencySnapshotEvent
import io.constellationnetwork.schema._
import io.constellationnetwork.schema.address.Address
import io.constellationnetwork.schema.balance.{Amount, Balance}
Expand Down Expand Up @@ -99,8 +102,17 @@ object SnapshotProcessorSuite extends SimpleIOSuite with TransactionGenerator {
)
implicit0(hs: HasherSelector[IO]) = HasherSelector.forSyncAlwaysCurrent(h)
currencyEventsCutter = CurrencyEventsCutter.make[IO](None)
validationErrorStorage <- ValidationErrorStorage
.make[IO, CurrencySnapshotEvent, BlockRejectionReason](16, _ => IO.pure(List.empty))
.asResource
currencySnapshotCreator = CurrencySnapshotCreator
.make[IO](currencySnapshotAcceptanceManager, None, SnapshotSizeConfig(Long.MaxValue, Long.MaxValue), currencyEventsCutter)
.make[IO](
currencySnapshotAcceptanceManager,
None,
SnapshotSizeConfig(Long.MaxValue, Long.MaxValue),
currencyEventsCutter,
validationErrorStorage
)
currencySnapshotValidator = CurrencySnapshotValidator
.make[IO](currencySnapshotCreator, validators.signedValidator, None, None)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ trait CliMethod {
c.feeConfigs.get(environment).map(SortedMap.from(_)).getOrElse(SortedMap.empty),
c.forkInfoStorage,
c.lastKryoHashOrdinal,
c.addresses
c.addresses,
c.validationErrorStorage.getOrElse(ValidationErrorStorageConfig.default)
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ object types {
forkInfoStorage: ForkInfoStorageConfig,
priorityPeerIds: Map[AppEnvironment, NonEmptySet[PeerId]],
lastKryoHashOrdinal: Map[AppEnvironment, SnapshotOrdinal],
addresses: AddressesConfig
addresses: AddressesConfig,
validationErrorStorage: Option[ValidationErrorStorageConfig]
)

case class SharedConfig(
Expand All @@ -46,7 +47,8 @@ object types {
feeConfigs: SortedMap[SnapshotOrdinal, FeeCalculatorConfig],
forkInfoStorage: ForkInfoStorageConfig,
lastKryoHashOrdinal: Map[AppEnvironment, SnapshotOrdinal],
addresses: AddressesConfig
addresses: AddressesConfig,
validationErrorStorage: ValidationErrorStorageConfig
)

case class SharedTrustConfig(
Expand Down Expand Up @@ -147,4 +149,13 @@ object types {

case class AddressesConfig(locked: Set[Address])

case class ValidationErrorStorageConfig(size: PosInt)

object ValidationErrorStorageConfig {

/** We are comfortable storing up to 100 MB of error data, and assuming the average rejected block size is a few kilobytes, the default
* storage capacity is set to 10,000 entries.
*/
def default: ValidationErrorStorageConfig = ValidationErrorStorageConfig(size = PosInt(10_000))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,35 @@ import io.constellationnetwork.schema.{BlockReference, SnapshotOrdinal}
import io.constellationnetwork.security.hash.Hash

import derevo.cats.{eqv, show}
import derevo.circe.magnolia.encoder
import derevo.derive

// NOTE: @derive(eqv, show) should not be required for each individual case class, but otherwise it throws in runtime

@derive(eqv, show)
sealed trait BlockNotAcceptedReason

@derive(eqv, show)
@derive(eqv, show, encoder)
sealed trait BlockRejectionReason extends BlockNotAcceptedReason

@derive(eqv, show)
@derive(eqv, show, encoder)
case class ValidationFailed(reasons: NonEmptyList[BlockValidationError]) extends BlockRejectionReason

@derive(eqv, show)
@derive(eqv, show, encoder)
case class ParentNotFound(parent: BlockReference) extends BlockRejectionReason

@derive(eqv, show)
@derive(eqv, show, encoder)
case object SnapshotOrdinalUnavailable extends BlockRejectionReason

@derive(eqv, show, encoder)
case object InvalidCurrencyMessageEvent extends BlockRejectionReason

@derive(eqv, show)
case class RejectedTransaction(tx: TransactionReference, reason: TransactionRejectionReason) extends BlockRejectionReason

@derive(eqv, show, encoder)
case class DataBlockNotAccepted(reason: String) extends BlockRejectionReason

@derive(eqv, show)
sealed trait BlockAwaitReason extends BlockNotAcceptedReason

Expand All @@ -53,7 +60,7 @@ sealed trait TransactionAwaitReason
case class ParentOrdinalAboveLastTxOrdinal(parentOrdinal: TransactionOrdinal, lastTxOrdinal: TransactionOrdinal)
extends TransactionAwaitReason

@derive(eqv, show)
@derive(eqv, show, encoder)
sealed trait TransactionRejectionReason

@derive(eqv, show)
Expand Down
Loading
Loading