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

Refactor block validation logic - effects #3808

Draft
wants to merge 35 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f541f9a
blockSignature function moved to BlockValidationLogic
stanislavlyalin Aug 8, 2022
179a7b5
formatOfFields function moved to BlockValidationLogic
stanislavlyalin Aug 8, 2022
ee167f4
version function moved to BlockValidationLogic
stanislavlyalin Aug 8, 2022
41176d5
futureTransaction function moved to BlockValidationLogic
stanislavlyalin Aug 8, 2022
c8d471c
transactionExpiration function moved to BlockValidationLogic
stanislavlyalin Aug 8, 2022
cc5dfef
deploysShardIdentifier function moved to BlockValidationLogic
stanislavlyalin Aug 8, 2022
beebdee
blockHash function moved to BlockValidationLogic
stanislavlyalin Aug 8, 2022
5972692
Removed unused
stanislavlyalin Aug 9, 2022
c1a1116
phloPrice function moved to BlockValidationLogic
stanislavlyalin Aug 9, 2022
0417ab5
Rearranged validation functions
stanislavlyalin Aug 9, 2022
be920f5
Version validation test moved to BlockValidationLogicSpec
stanislavlyalin Aug 9, 2022
0af307a
Block hash validation test moved to BlockValidationLogicSpec
stanislavlyalin Aug 9, 2022
31cd7dc
Block fields validation test moved to BlockValidationLogicSpec
stanislavlyalin Aug 9, 2022
19f56a4
Block signature validation tests moved to BlockValidationLogicSpec
stanislavlyalin Aug 9, 2022
6b5dc7f
Future deploy validation tests moved to BlockValidationLogicSpec
stanislavlyalin Aug 9, 2022
95e4a12
Deploy expiration validation tests moved to BlockValidationLogicSpec
stanislavlyalin Aug 9, 2022
48a557e
Internal function `ignore` marked private
stanislavlyalin Aug 9, 2022
962b06a
Fixed test name
stanislavlyalin Aug 9, 2022
91a6f8e
Simplified validation functions. They returns just Boolean
stanislavlyalin Aug 17, 2022
9194775
Fixed calling validation functions. Added related tests
stanislavlyalin Aug 18, 2022
e345109
Test for `formatOfFields` made generative
stanislavlyalin Aug 18, 2022
9f5a5a8
Test for block signature made generative
stanislavlyalin Aug 19, 2022
d5b040d
Test for future deploy made generative
stanislavlyalin Aug 19, 2022
2f63371
Fixed review issue: `f` evaluated always, `errorStatus` only if `f` r…
stanislavlyalin Aug 23, 2022
ddf248c
Tests for block's signature algorithm made generative
stanislavlyalin Aug 23, 2022
1375491
Refactored first test of ValidateTest with MonixTaskTest and Mockito.…
stanislavlyalin Aug 9, 2022
29eb860
Refactored two more tests
stanislavlyalin Aug 10, 2022
34026eb
Extended signature algorithm test
stanislavlyalin Aug 24, 2022
d46c9b5
Removed genesisContext and genesis
stanislavlyalin Aug 25, 2022
43be59a
Refactored createChain function
stanislavlyalin Aug 25, 2022
6cba86e
Removed signedBlock function
stanislavlyalin Aug 26, 2022
7f8e2f2
Refactored one more test
stanislavlyalin Aug 26, 2022
75e34d3
Refactored sequence number validation tests
stanislavlyalin Aug 26, 2022
c53c56d
Refactored repeat deploy validation tests
stanislavlyalin Aug 26, 2022
e494319
Refactored block summary validation tests
stanislavlyalin Aug 26, 2022
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
@@ -0,0 +1,57 @@
package coop.rchain.casper

import cats.syntax.all._
import coop.rchain.casper.Validate.signatureVerifiers
import coop.rchain.casper.protocol.BlockMessage
import coop.rchain.casper.util.ProtoUtil
import coop.rchain.models.BlockVersion
import coop.rchain.models.syntax._

import scala.util.{Success, Try}

object BlockValidationLogic {
def version(b: BlockMessage): Boolean = BlockVersion.Supported.contains(b.version)

def blockHash(b: BlockMessage): Boolean = b.blockHash == ProtoUtil.hashBlock(b)

def formatOfFields(b: BlockMessage): Boolean =
b.blockHash.nonEmpty &&
b.sig.nonEmpty &&
b.sigAlgorithm.nonEmpty &&
b.shardId.nonEmpty &&
b.postStateHash.nonEmpty

def blockSignature(b: BlockMessage): Boolean =
signatureVerifiers
.get(b.sigAlgorithm)
.exists(verify => {
Try(verify(b.blockHash.toByteArray, b.sig.toByteArray, b.sender.toByteArray)) match {
case Success(true) => true
case _ => false
}
})

def futureTransaction(b: BlockMessage): Boolean =
!b.state.deploys.map(_.deploy).exists(_.data.validAfterBlockNumber > b.blockNumber)

def transactionExpiration(b: BlockMessage, expirationThreshold: Int): Boolean = {
val earliestAcceptableValidAfterBlockNumber = b.blockNumber - expirationThreshold
!b.state.deploys
.map(_.deploy)
.exists(_.data.validAfterBlockNumber <= earliestAcceptableValidAfterBlockNumber)
}

/**
* Validator should only process deploys from its own shard with shard names in ASCII characters only
*/
def deploysShardIdentifier(b: BlockMessage, shardId: String): Boolean = {
assert(shardId.onlyAscii, "Shard name should contain only ASCII characters")
b.state.deploys.forall(_.deploy.data.shardId == shardId)
}

/**
* All of deploys must have greater or equal phloPrice then minPhloPrice
*/
def phloPrice(b: BlockMessage, minPhloPrice: Long): Boolean =
b.state.deploys.forall(_.deploy.data.phloPrice >= minPhloPrice)
}
18 changes: 7 additions & 11 deletions casper/src/main/scala/coop/rchain/casper/MultiParentCasper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,13 @@ object MultiParentCasper {

// This validation is only to punish validator which accepted lower price deploys.
// And this can happen if not configured correctly.
status <- EitherT(Validate.phloPrice(block, minPhloPrice))
.recoverWith {
case _ =>
val warnToLog = EitherT.liftF[F, InvalidBlock, Unit](
Log[F].warn(s"One or more deploys has phloPrice lower than $minPhloPrice")
)
val asValid = EitherT.rightT[F, InvalidBlock](BlockStatus.valid)
warnToLog *> asValid
}
.as(blockMetadata)
.leftMap(e => (blockMetadata, e))
status <- {
val warnToLog = EitherT.liftF[F, InvalidBlock, Unit](
Log[F].warn(s"One or more deploys has phloPrice lower than $minPhloPrice")
)
(warnToLog.whenA(!BlockValidationLogic.phloPrice(block, minPhloPrice)) *>
EitherT.rightT[F, InvalidBlock](blockMetadata)).leftMap(e => (blockMetadata, e))
}
_ <- EitherT.liftF(Span[F].mark("phlogiston-price-validated"))
} yield status

Expand Down
164 changes: 17 additions & 147 deletions casper/src/main/scala/coop/rchain/casper/Validate.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package coop.rchain.casper

import cats.Monad
import cats.data.EitherT
import cats.effect.{Concurrent, Sync}
import cats.syntax.all._
import cats.{Applicative, Monad}
import com.google.protobuf.ByteString
import coop.rchain.blockstorage.BlockStore
import coop.rchain.blockstorage.BlockStore.BlockStore
Expand All @@ -15,85 +15,34 @@ import coop.rchain.casper.util.ProtoUtil
import coop.rchain.crypto.signatures.Secp256k1
import coop.rchain.dag.DagOps
import coop.rchain.metrics.{Metrics, Span}
import coop.rchain.models.{BlockMetadata, BlockVersion}
import coop.rchain.models.syntax._
import coop.rchain.models.BlockMetadata
import coop.rchain.shared._

import scala.util.{Success, Try}

// TODO: refactor all validation functions to separate logging from actual validation logic
object Validate {
type PublicKey = Array[Byte]
type Data = Array[Byte]
type Signature = Array[Byte]

val DRIFT = 15000 // 15 seconds
implicit private val logSource: LogSource = LogSource(this.getClass)
val signatureVerifiers: Map[String, (Data, Signature, PublicKey) => Boolean] =
Map(
"secp256k1" -> Secp256k1.verify
)

def ignore(b: BlockMessage, reason: String): String =
private def ignore(b: BlockMessage, reason: String): String =
s"Ignoring block ${PrettyPrinter.buildString(b.blockHash)} because $reason"

/* Validation of block with logging included */

def blockSignature[F[_]: Applicative: Log](b: BlockMessage): F[Boolean] =
signatureVerifiers
.get(b.sigAlgorithm)
.map(verify => {
Try(verify(b.blockHash.toByteArray, b.sig.toByteArray, b.sender.toByteArray)) match {
case Success(true) => true.pure
case _ => Log[F].warn(ignore(b, "signature is invalid.")).map(_ => false)
}
}) getOrElse {
for {
_ <- Log[F].warn(ignore(b, s"signature algorithm ${b.sigAlgorithm} is unsupported."))
} yield false
}

def formatOfFields[F[_]: Monad: Log](b: BlockMessage): F[Boolean] =
if (b.blockHash.isEmpty) {
for {
_ <- Log[F].warn(ignore(b, s"block hash is empty."))
} yield false
} else if (b.sig.isEmpty) {
for {
_ <- Log[F].warn(ignore(b, s"block signature is empty."))
} yield false
} else if (b.sigAlgorithm.isEmpty) {
for {
_ <- Log[F].warn(ignore(b, s"block signature algorithm is empty."))
} yield false
} else if (b.shardId.isEmpty) {
for {
_ <- Log[F].warn(ignore(b, s"block shard identifier is empty."))
} yield false
} else if (b.postStateHash.isEmpty) {
for {
_ <- Log[F].warn(ignore(b, s"block post state hash is empty."))
} yield false
} else {
true.pure
}

def version[F[_]: Monad: Log](b: BlockMessage): F[Boolean] = {
val blockVersion = b.version
if (BlockVersion.Supported.contains(blockVersion)) {
true.pure
} else {
val versionsStr = BlockVersion.Supported.mkString(" or ")
val msg = s"received block version $blockVersion is not the expected version $versionsStr."
Log[F].warn(ignore(b, msg)).as(false)
}
}

def blockSummary[F[_]: Sync: BlockDagStorage: BlockStore: Log: Metrics: Span](
block: BlockMessage,
shardId: String,
expirationThreshold: Int
): F[ValidBlockProcessing] =
): F[ValidBlockProcessing] = {
def validate(f: Boolean, errorStatus: => InvalidBlock): EitherT[F, InvalidBlock, ValidBlock] =
EitherT.fromOption(Option(BlockStatus.valid).filter(_ => f), errorStatus)

(for {
// First validate justifications because they are basis for all other validation
_ <- EitherT.liftF(Span[F].mark("before-justification-regression-validation"))
Expand All @@ -106,14 +55,21 @@ object Validate {
_ <- EitherT(Validate.blockNumber(block))
// Deploys validation
_ <- EitherT.liftF(Span[F].mark("before-deploys-shard-identifier-validation"))
_ <- EitherT(Validate.deploysShardIdentifier(block, shardId))
_ <- validate(
BlockValidationLogic.deploysShardIdentifier(block, shardId),
BlockStatus.invalidDeployShardId
)
_ <- EitherT.liftF(Span[F].mark("before-future-transaction-validation"))
_ <- EitherT(Validate.futureTransaction(block))
_ <- validate(BlockValidationLogic.futureTransaction(block), BlockStatus.containsFutureDeploy)
_ <- EitherT.liftF(Span[F].mark("before-transaction-expired-validation"))
_ <- EitherT(Validate.transactionExpiration(block, expirationThreshold))
_ <- validate(
BlockValidationLogic.transactionExpiration(block, expirationThreshold),
BlockStatus.containsExpiredDeploy
)
_ <- EitherT.liftF(Span[F].mark("before-repeat-deploy-validation"))
s <- EitherT(Validate.repeatDeploy(block, expirationThreshold))
} yield s).value
}

/**
* Validate no deploy with the same sig has been produced in the chain
Expand Down Expand Up @@ -197,46 +153,6 @@ object Validate {
}
} yield status

def futureTransaction[F[_]: Monad: Log](b: BlockMessage): F[ValidBlockProcessing] = {
val blockNumber = b.blockNumber
val deploys = b.state.deploys.map(_.deploy)
val maybeFutureDeploy = deploys.find(_.data.validAfterBlockNumber > blockNumber)
maybeFutureDeploy
.traverse { futureDeploy =>
Log[F]
.warn(
ignore(
b,
s"block contains an future deploy with valid after block number of ${futureDeploy.data.validAfterBlockNumber}: ${futureDeploy.data.term}"
)
)
.as(BlockStatus.containsFutureDeploy)
}
.map(maybeError => maybeError.toLeft(BlockStatus.valid))
}

def transactionExpiration[F[_]: Monad: Log](
b: BlockMessage,
expirationThreshold: Int
): F[ValidBlockProcessing] = {
val earliestAcceptableValidAfterBlockNumber = b.blockNumber - expirationThreshold
val deploys = b.state.deploys.map(_.deploy)
val maybeExpiredDeploy =
deploys.find(_.data.validAfterBlockNumber <= earliestAcceptableValidAfterBlockNumber)
maybeExpiredDeploy
.traverse { expiredDeploy =>
Log[F]
.warn(
ignore(
b,
s"block contains an expired deploy with valid after block number of ${expiredDeploy.data.validAfterBlockNumber}: ${expiredDeploy.data.term}"
)
)
.as(BlockStatus.containsExpiredDeploy)
}
.map(maybeError => maybeError.toLeft(BlockStatus.valid))
}

/**
* Works with either efficient justifications or full explicit justifications.
* Specifically, with efficient justifications, if a block B doesn't update its
Expand Down Expand Up @@ -264,39 +180,6 @@ object Validate {
}
} yield status

// Validator should only process deploys from its own shard with shard names in ASCII characters only
def deploysShardIdentifier[F[_]: Monad: Log](
b: BlockMessage,
shardId: String
): F[ValidBlockProcessing] = {
assert(shardId.onlyAscii, "Shard name should contain only ASCII characters")
if (b.state.deploys.forall(_.deploy.data.shardId == shardId)) {
BlockStatus.valid.asRight[InvalidBlock].pure
} else {
for {
_ <- Log[F].warn(ignore(b, s"not for all deploys shard identifier is $shardId."))
} yield BlockStatus.invalidDeployShardId.asLeft[ValidBlock]
}
}

def blockHash[F[_]: Applicative: Log](b: BlockMessage): F[Boolean] = {
val blockHashComputed = ProtoUtil.hashBlock(b)
if (b.blockHash == blockHashComputed)
true.pure
else {
val computedHashString = PrettyPrinter.buildString(blockHashComputed)
val hashString = PrettyPrinter.buildString(b.blockHash)
for {
_ <- Log[F].warn(
ignore(
b,
s"block hash $hashString does not match to computed value $computedHashString."
)
)
} yield false
}
}

/**
* Justification regression check.
* Compares justifications that has been already used by sender and recorded in the DAG with
Expand Down Expand Up @@ -375,17 +258,4 @@ object Validate {
}
}
}

/**
* All of deploys must have greater or equal phloPrice then minPhloPrice
*/
def phloPrice[F[_]: Log: Concurrent](
b: BlockMessage,
minPhloPrice: Long
): F[ValidBlockProcessing] =
if (b.state.deploys.forall(_.deploy.data.phloPrice >= minPhloPrice)) {
BlockStatus.valid.asRight[InvalidBlock].pure
} else {
BlockStatus.containsLowCostDeploy.asLeft[ValidBlock].pure
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import coop.rchain.blockstorage.BlockStore.BlockStore
import coop.rchain.blockstorage.dag.BlockDagStorage
import coop.rchain.casper.protocol.BlockMessage
import coop.rchain.casper.syntax._
import coop.rchain.casper.{PrettyPrinter, Validate}
import coop.rchain.casper.{BlockValidationLogic, PrettyPrinter}
import coop.rchain.models.BlockHash.BlockHash
import coop.rchain.shared.Log
import coop.rchain.shared.syntax._
Expand Down Expand Up @@ -205,14 +205,14 @@ object BlockReceiver {
// TODO: 1. validation and logging in these checks should be separated
// 2. logging of these block should indicate that information cannot be trusted
// e.g. if block hash is invalid it cannot represent identity of a block
val validFormat = Validate.formatOfFields(b)
val validHash = Validate.blockHash(b)
val validSig = Validate.blockSignature(b)
val validFormat = BlockValidationLogic.formatOfFields(b)
val validHash = BlockValidationLogic.blockHash(b)
val validSig = BlockValidationLogic.blockSignature(b)
// TODO: check sender to be valid bonded validator
// - not always possible because now are new blocks downloaded from DAG tips
// which in case of epoch change sender can be unknown
// TODO: check valid version (possibly part of hash checking)
validFormat &&^ validShard &&^ validHash &&^ validSig
validShard &&^ (validFormat && validHash && validSig).pure
}

// Check if block should be stored
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package coop.rchain.casper.engine

import cats.effect.concurrent.{Deferred, Ref}
import cats.effect.{Concurrent, Timer}
import cats.effect.{Concurrent, Sync, Timer}
import cats.syntax.all._
import coop.rchain.blockstorage.BlockStore
import coop.rchain.blockstorage.BlockStore.BlockStore
Expand Down Expand Up @@ -143,7 +143,7 @@ class NodeSyncing[F[_]
BlockStore[F].contains(_),
BlockStore[F].getUnsafe,
BlockStore[F].put(_, _),
Validate.blockHash[F]
block => Sync[F].delay(BlockValidationLogic.blockHash(block))
)

// Request tuple space state for Last Finalized State
Expand Down
Loading