Skip to content

[ETCM-72] Checkpoint Block validation #706

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

Merged
merged 1 commit into from
Oct 1, 2020
Merged
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 @@ -44,7 +44,8 @@ object BlockchainTestConfig {
aghartaBlockNumber = Long.MaxValue,
phoenixBlockNumber = Long.MaxValue,
ecip1098BlockNumber = Long.MaxValue,
treasuryAddress = Address(0)
treasuryAddress = Address(0),
ecip1097BlockNumber = Long.MaxValue
)

val FrontierConfig = BaseBlockchainConfig.copy(
Expand Down
3 changes: 2 additions & 1 deletion src/it/scala/io/iohk/ethereum/txExecTest/ECIP1017Test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class ECIP1017Test extends AnyFlatSpec with Matchers {
phoenixBlockNumber = Long.MaxValue,
petersburgBlockNumber = Long.MaxValue,
ecip1098BlockNumber = Long.MaxValue,
treasuryAddress = Address(0)
treasuryAddress = Address(0),
ecip1097BlockNumber = Long.MaxValue
)
val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))

Expand Down
3 changes: 2 additions & 1 deletion src/it/scala/io/iohk/ethereum/txExecTest/ForksTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class ForksTest extends AnyFlatSpec with Matchers {
phoenixBlockNumber = Long.MaxValue,
petersburgBlockNumber = Long.MaxValue,
ecip1098BlockNumber = Long.MaxValue,
treasuryAddress = Address(0)
treasuryAddress = Address(0),
ecip1097BlockNumber = Long.MaxValue
)

val noErrors = a[Right[_, Seq[Receipt]]]
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/chains/etc-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
treasury-address = "0011223344556677889900112233445566778899"
ecip1098-block-number = "1000000000000000000"

# Checkpointing fork block number
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
ecip1097-block-number = "1000000000000000000"

# DAO fork configuration (Ethereum HF/Classic split)
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
dao {
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/chains/eth-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
treasury-address = "0011223344556677889900112233445566778899"
ecip1098-block-number = "1000000000000000000"

# Checkpointing fork block number
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
ecip1097-block-number = "1000000000000000000"

# DAO fork configuration (Ethereum HF/Classic split)
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
dao {
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/chains/mordor-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
treasury-address = "0011223344556677889900112233445566778899"
ecip1098-block-number = "1000000000000000000"

# Checkpointing fork block number
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
ecip1097-block-number = "1000000000000000000"

# DAO fork configuration (Ethereum HF/Classic split)
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
dao = null
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/chains/ropsten-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
treasury-address = "0011223344556677889900112233445566778899"
ecip1098-block-number = "1000000000000000000"

# Checkpointing fork block number
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
ecip1097-block-number = "1000000000000000000"

# DAO fork configuration (Ethereum HF/Classic split)
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
dao {
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/chains/test-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
treasury-address = "0011223344556677889900112233445566778899"
ecip1098-block-number = "1000000000000000000"

# Checkpointing fork block number
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
ecip1097-block-number = "1000000000000000000"

# DAO fork configuration (Ethereum HF/Classic split)
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
dao {
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/chains/testnet-internal-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
treasury-address = "0011223344556677889900112233445566778899"
ecip1098-block-number = "1000000000000000000"

# Checkpointing fork block number
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
ecip1097-block-number = "1000000000000000000"

# DAO fork configuration (Ethereum HF/Classic split)
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
dao = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class EthashDifficultyCalculator(blockchainConfig: BlockchainConfig) extends Dif
if (blockNumber < homesteadBlockNumber) {
if (blockTimestamp < parentHeader.unixTimestamp + 13) 1 else -1
} else if (blockNumber >= byzantiumBlockNumber || blockNumber >= blockchainConfig.atlantisBlockNumber) {
val parentUncleFactor = if (parentHeader.ommersHash == BlockHeader.emptyOmmerHash) 1 else 2
val parentUncleFactor = if (parentHeader.ommersHash == BlockHeader.EmptyOmmers) 1 else 2
math.max(parentUncleFactor - (timestampDiff / 9), FrontierTimestampDiffLimit)
} else {
math.max(1 - (timestampDiff / 10), FrontierTimestampDiffLimit)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.iohk.ethereum.consensus
package validators

import io.iohk.ethereum.crypto.ECDSASignature
import io.iohk.ethereum.domain.BlockHeader

/**
Expand Down Expand Up @@ -33,6 +34,14 @@ object BlockHeaderError {
case object HeaderNumberError extends BlockHeaderError
case object HeaderPoWError extends BlockHeaderError
case class HeaderOptOutError(ecip1098Activated: Boolean, optOutDefined: Boolean) extends BlockHeaderError
case class HeaderWrongNumberOfCheckpointSignatures(sigCount: Int) extends BlockHeaderError
case class HeaderInvalidCheckpointSignatures(invalidSignaturesWithPublics: Seq[(ECDSASignature, Option[String])])
extends BlockHeaderError
case object HeaderCheckpointTooEarly extends BlockHeaderError
case class HeaderFieldNotEmptyError(msg: String) extends BlockHeaderError
case class HeaderNotMatchParentError(msg: String) extends BlockHeaderError

case class HeaderUnexpectedError(msg: String) extends BlockHeaderError
}

sealed trait BlockHeaderValid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ abstract class BlockHeaderValidatorSkeleton(blockchainConfig: BlockchainConfig)

import BlockHeaderValidator._

private val blockWithCheckpointHeaderValidator = new BlockWithCheckpointHeaderValidator(blockchainConfig)

/**
* The difficulty calculator. This is specific to the consensus protocol.
*/
Expand All @@ -39,6 +41,33 @@ abstract class BlockHeaderValidatorSkeleton(blockchainConfig: BlockchainConfig)
* @param parentHeader BlockHeader of the parent of the block to validate.
*/
def validate(blockHeader: BlockHeader, parentHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
if(blockHeader.hasCheckpoint) validateBlockWithCheckpointHeader(blockHeader, parentHeader)
else validateRegularHeader(blockHeader, parentHeader)
}

/** This method allows validate a BlockHeader (stated on
* section 4.4.4 of http://paper.gavwood.com/).
*
* @param blockHeader BlockHeader to validate.
* @param getBlockHeaderByHash function to obtain the parent.
*/
def validate(blockHeader: BlockHeader, getBlockHeaderByHash: GetBlockHeaderByHash): Either[BlockHeaderError, BlockHeaderValid] = {
for {
blockHeaderParent <- getBlockHeaderByHash(blockHeader.parentHash).map(Right(_)).getOrElse(Left(HeaderParentNotFoundError))
_ <- validate(blockHeader, blockHeaderParent)
} yield BlockHeaderValid
}

/** This method runs a validation of the header of regular block.
* It runs basic validation and pow validation (hidden in validateEvenMore)
*
* @param blockHeader BlockHeader to validate.
* @param parentHeader BlockHeader of the parent of the block to validate.
*/
private def validateRegularHeader(
blockHeader: BlockHeader,
parentHeader: BlockHeader
): Either[BlockHeaderError, BlockHeaderValid] = {
for {
// NOTE how we include everything except PoW (which is deferred to `validateEvenMore`),
// and that difficulty validation is in effect abstract (due to `difficulty`).
Expand All @@ -53,16 +82,19 @@ abstract class BlockHeaderValidatorSkeleton(blockchainConfig: BlockchainConfig)
} yield BlockHeaderValid
}

/** This method allows validate a BlockHeader (stated on
* section 4.4.4 of http://paper.gavwood.com/).
*
* @param blockHeader BlockHeader to validate.
* @param getBlockHeaderByHash function to obtain the parent.
*/
def validate(blockHeader: BlockHeader, getBlockHeaderByHash: GetBlockHeaderByHash): Either[BlockHeaderError, BlockHeaderValid] = {
/** This method runs a validation of the header of block with checkpoint.
* It runs basic validation and checkpoint specific validation
*
* @param blockHeader BlockHeader to validate.
* @param parentHeader BlockHeader of the parent of the block to validate.
*/
private def validateBlockWithCheckpointHeader(
blockHeader: BlockHeader,
parentHeader: BlockHeader
): Either[BlockHeaderError, BlockHeaderValid] = {
for {
blockHeaderParent <- getBlockHeaderByHash(blockHeader.parentHash).map(Right(_)).getOrElse(Left(HeaderParentNotFoundError))
_ <- validate(blockHeader, blockHeaderParent)
_ <- blockWithCheckpointHeaderValidator.validate(blockHeader, parentHeader)
_ <- validateNumber(blockHeader, parentHeader)
} yield BlockHeaderValid
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package io.iohk.ethereum.consensus.validators

import io.iohk.ethereum.consensus.validators.BlockHeaderError._
import io.iohk.ethereum.domain.BlockHeader
import io.iohk.ethereum.ledger.BloomFilter
import io.iohk.ethereum.utils.{BlockchainConfig, ByteStringUtils}

/** Validator specialized for the block with checkpoint
*
* @param blockchainConfig
*/
class BlockWithCheckpointHeaderValidator(blockchainConfig: BlockchainConfig) {

def validate(blockHeader: BlockHeader, parentHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
for {
_ <- validateECIP1097Number(blockHeader)
_ <- validateCheckpointSignatures(blockHeader, parentHeader)
_ <- validateEmptyFields(blockHeader)
_ <- validateFieldsCopiedFromParent(blockHeader, parentHeader)
_ <- validateGasUsed(blockHeader)
_ <- validateTimestamp(blockHeader, parentHeader)
} yield BlockHeaderValid
}

/**
* Validates [[io.iohk.ethereum.domain.BlockHeader.checkpoint]] signatures
*
* @param blockHeader BlockHeader to validate.
* @param parentHeader BlockHeader of the parent of the block to validate.
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderInvalidCheckpointSignatures]] otherwise
*/
private def validateCheckpointSignatures(
blockHeader: BlockHeader,
parentHeader: BlockHeader
): Either[BlockHeaderError, BlockHeaderValid] = {
blockHeader.checkpoint
.map { checkpoint =>
lazy val signaturesWithRecoveredKeys = checkpoint.signatures.map(s => s -> s.publicKey(parentHeader.hash))

// if at least 2 different signatures came from the same signer it will be in this set (also takes care
// of duplicate signatures)
lazy val repeatedSigners = signaturesWithRecoveredKeys
.groupBy(_._2)
.filter(_._2.size > 1)
.keySet
.flatten

lazy val (validSignatures, invalidSignatures) = signaturesWithRecoveredKeys.partition {
//signatures are valid if the signers are known AND distinct
case (sig, Some(pk)) => blockchainConfig.checkpointPubKeys.contains(pk) && !repeatedSigners.contains(pk)
case _ => false
}

// we fail fast if there are too many signatures (DoS protection)
if (checkpoint.signatures.size > blockchainConfig.checkpointPubKeys.size)
Left(HeaderWrongNumberOfCheckpointSignatures(checkpoint.signatures.size))
else if (invalidSignatures.nonEmpty) {
val sigsWithKeys = invalidSignatures.map {
case (sig, maybePk) => (sig, maybePk.map(ByteStringUtils.hash2string))
}
Left(HeaderInvalidCheckpointSignatures(sigsWithKeys))
} else if (validSignatures.size < blockchainConfig.minRequireSignatures)
Left(HeaderWrongNumberOfCheckpointSignatures(validSignatures.size))
else
Right(BlockHeaderValid)
}
.getOrElse(Left(HeaderUnexpectedError("Attempted to validate a checkpoint on a block without a checkpoint")))
}

/**
* Validates emptiness of:
* - beneficiary
* - extraData
* - treasuryOptOut
* - ommersHash
* - transactionsRoot
* - receiptsRoot
* - logsBloom
* - nonce
* - mixHash
*
* @param blockHeader BlockHeader to validate.
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderFieldNotEmptyError]] otherwise
*/
private def validateEmptyFields(blockHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
if (blockHeader.beneficiary != BlockHeader.EmptyBeneficiary)
notEmptyFieldError("beneficiary")
else if (blockHeader.ommersHash != BlockHeader.EmptyOmmers)
notEmptyFieldError("ommersHash")
else if (blockHeader.transactionsRoot != BlockHeader.EmptyMpt)
notEmptyFieldError("transactionsRoot")
else if (blockHeader.receiptsRoot != BlockHeader.EmptyMpt)
notEmptyFieldError("receiptsRoot")
else if (blockHeader.logsBloom != BloomFilter.EmptyBloomFilter)
notEmptyFieldError("logsBloom")
else if (blockHeader.extraData.nonEmpty)
notEmptyFieldError("extraData")
else if (blockHeader.treasuryOptOut.isDefined)
notEmptyFieldError("treasuryOptOut")
else if (blockHeader.nonce.nonEmpty)
notEmptyFieldError("nonce")
else if (blockHeader.mixHash.nonEmpty)
notEmptyFieldError("mixHash")
else Right(BlockHeaderValid)
}

private def notEmptyFieldError(field: String) = Left(HeaderFieldNotEmptyError(s"$field is not empty"))

/**
* Validates fields which should be equal to parent equivalents:
* - stateRoot
*
* @param blockHeader BlockHeader to validate.
* @param parentHeader BlockHeader of the parent of the block to validate.
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderNotMatchParentError]] otherwise
*/
private def validateFieldsCopiedFromParent(
blockHeader: BlockHeader,
parentHeader: BlockHeader
): Either[BlockHeaderError, BlockHeaderValid] = {
if (blockHeader.stateRoot != parentHeader.stateRoot)
fieldNotMatchedParentFieldError("stateRoot")
else if (blockHeader.gasLimit != parentHeader.gasLimit)
fieldNotMatchedParentFieldError("gasLimit")
else if (blockHeader.difficulty != parentHeader.difficulty)
fieldNotMatchedParentFieldError("difficulty")
else Right(BlockHeaderValid)
}

private def fieldNotMatchedParentFieldError(field: String) =
Left(HeaderNotMatchParentError(s"$field has different value that similar parent field"))


/**
* Validates gasUsed equal to zero
* @param blockHeader BlockHeader to validate.
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderGasUsedError]] otherwise
*/
private def validateGasUsed(blockHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
if (blockHeader.gasUsed != BigInt(0)) Left(HeaderGasUsedError)
else Right(BlockHeaderValid)
}

/**
* Validates [[io.iohk.ethereum.domain.BlockHeader.unixTimestamp]] is one bigger than parent unixTimestamp
*
* @param blockHeader BlockHeader to validate.
* @param parentHeader BlockHeader of the parent of the block to validate.
* @return BlockHeader if valid, an [[HeaderTimestampError]] otherwise
*/
private def validateTimestamp(
blockHeader: BlockHeader,
parentHeader: BlockHeader
): Either[BlockHeaderError, BlockHeaderValid] =
if (blockHeader.unixTimestamp == parentHeader.unixTimestamp + 1) Right(BlockHeaderValid)
else Left(HeaderTimestampError)

/**
* Validates [[io.iohk.ethereum.domain.BlockHeader.checkpoint]] is only defined if ECIP1097 is enabled at the block's number
*
* @param blockHeader BlockHeader to validate.
* @return BlockHeader if valid, an [[HeaderCheckpointTooEarly]] otherwise
*/
private def validateECIP1097Number(blockHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
if (blockHeader.number >= blockchainConfig.ecip1097BlockNumber) Right(BlockHeaderValid)
else Left(HeaderCheckpointTooEarly)
}
}
Loading