Skip to content

Commit ba02fcc

Browse files
committed
etcm-72 added checkpoint block validator
1 parent 3726c6f commit ba02fcc

25 files changed

+757
-40
lines changed

src/ets/scala/io/iohk/ethereum/ets/blockchain/BlockchainTestConfig.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ object BlockchainTestConfig {
4444
aghartaBlockNumber = Long.MaxValue,
4545
phoenixBlockNumber = Long.MaxValue,
4646
ecip1098BlockNumber = Long.MaxValue,
47-
treasuryAddress = Address(0)
47+
treasuryAddress = Address(0),
48+
ecip1097BlockNumber = Long.MaxValue
4849
)
4950

5051
val FrontierConfig = BaseBlockchainConfig.copy(

src/it/scala/io/iohk/ethereum/txExecTest/ECIP1017Test.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ class ECIP1017Test extends AnyFlatSpec with Matchers {
4646
phoenixBlockNumber = Long.MaxValue,
4747
petersburgBlockNumber = Long.MaxValue,
4848
ecip1098BlockNumber = Long.MaxValue,
49-
treasuryAddress = Address(0)
49+
treasuryAddress = Address(0),
50+
ecip1097BlockNumber = Long.MaxValue
5051
)
5152
val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
5253

src/it/scala/io/iohk/ethereum/txExecTest/ForksTest.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ class ForksTest extends AnyFlatSpec with Matchers {
4545
phoenixBlockNumber = Long.MaxValue,
4646
petersburgBlockNumber = Long.MaxValue,
4747
ecip1098BlockNumber = Long.MaxValue,
48-
treasuryAddress = Address(0)
48+
treasuryAddress = Address(0),
49+
ecip1097BlockNumber = Long.MaxValue
4950
)
5051

5152
val noErrors = a[Right[_, Seq[Receipt]]]

src/main/resources/chains/etc-chain.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
treasury-address = "0011223344556677889900112233445566778899"
8484
ecip1098-block-number = "1000000000000000000"
8585

86+
# Checkpointing fork block number
87+
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
88+
ecip1097-block-number = "1000000000000000000"
89+
8690
# DAO fork configuration (Ethereum HF/Classic split)
8791
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
8892
dao {

src/main/resources/chains/eth-chain.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
treasury-address = "0011223344556677889900112233445566778899"
8484
ecip1098-block-number = "1000000000000000000"
8585

86+
# Checkpointing fork block number
87+
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
88+
ecip1097-block-number = "1000000000000000000"
89+
8690
# DAO fork configuration (Ethereum HF/Classic split)
8791
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
8892
dao {

src/main/resources/chains/mordor-chain.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
treasury-address = "0011223344556677889900112233445566778899"
8484
ecip1098-block-number = "1000000000000000000"
8585

86+
# Checkpointing fork block number
87+
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
88+
ecip1097-block-number = "1000000000000000000"
89+
8690
# DAO fork configuration (Ethereum HF/Classic split)
8791
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
8892
dao = null

src/main/resources/chains/ropsten-chain.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@
8686
treasury-address = "0011223344556677889900112233445566778899"
8787
ecip1098-block-number = "1000000000000000000"
8888

89+
# Checkpointing fork block number
90+
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
91+
ecip1097-block-number = "1000000000000000000"
92+
8993
# DAO fork configuration (Ethereum HF/Classic split)
9094
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
9195
dao {

src/main/resources/chains/test-chain.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
treasury-address = "0011223344556677889900112233445566778899"
8484
ecip1098-block-number = "1000000000000000000"
8585

86+
# Checkpointing fork block number
87+
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
88+
ecip1097-block-number = "1000000000000000000"
89+
8690
# DAO fork configuration (Ethereum HF/Classic split)
8791
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
8892
dao {

src/main/resources/chains/testnet-internal-chain.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
treasury-address = "0011223344556677889900112233445566778899"
8484
ecip1098-block-number = "1000000000000000000"
8585

86+
# Checkpointing fork block number
87+
# https://ecips.ethereumclassic.org/ECIPs/ecip-1097
88+
ecip1097-block-number = "1000000000000000000"
89+
8690
# DAO fork configuration (Ethereum HF/Classic split)
8791
# https://blog.ethereum.org/2016/07/20/hard-fork-completed/
8892
dao = null

src/main/scala/io/iohk/ethereum/consensus/ethash/difficulty/EthashDifficultyCalculator.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class EthashDifficultyCalculator(blockchainConfig: BlockchainConfig) extends Dif
2222
if (blockNumber < homesteadBlockNumber) {
2323
if (blockTimestamp < parentHeader.unixTimestamp + 13) 1 else -1
2424
} else if (blockNumber >= byzantiumBlockNumber || blockNumber >= blockchainConfig.atlantisBlockNumber) {
25-
val parentUncleFactor = if (parentHeader.ommersHash == BlockHeader.emptyOmmerHash) 1 else 2
25+
val parentUncleFactor = if (parentHeader.ommersHash == BlockHeader.EmptyOmmers) 1 else 2
2626
math.max(parentUncleFactor - (timestampDiff / 9), FrontierTimestampDiffLimit)
2727
} else {
2828
math.max(1 - (timestampDiff / 10), FrontierTimestampDiffLimit)

src/main/scala/io/iohk/ethereum/consensus/validators/BlockHeaderValidator.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.iohk.ethereum.consensus
22
package validators
33

4+
import io.iohk.ethereum.crypto.ECDSASignature
45
import io.iohk.ethereum.domain.BlockHeader
56

67
/**
@@ -33,6 +34,14 @@ object BlockHeaderError {
3334
case object HeaderNumberError extends BlockHeaderError
3435
case object HeaderPoWError extends BlockHeaderError
3536
case class HeaderOptOutError(ecip1098Activated: Boolean, optOutDefined: Boolean) extends BlockHeaderError
37+
case class HeaderWrongNumberOfCheckpointSignatures(sigCount: Int) extends BlockHeaderError
38+
case class HeaderInvalidCheckpointSignatures(invalidSignaturesWithPublics: Seq[(ECDSASignature, Option[String])])
39+
extends BlockHeaderError
40+
case object HeaderCheckpointTooEarly extends BlockHeaderError
41+
case class HeaderFieldNotEmptyError(msg: String) extends BlockHeaderError
42+
case class HeaderNotMatchParentError(msg: String) extends BlockHeaderError
43+
44+
case class HeaderUnexpectedError(msg: String) extends BlockHeaderError
3645
}
3746

3847
sealed trait BlockHeaderValid

src/main/scala/io/iohk/ethereum/consensus/validators/BlockHeaderValidatorSkeleton.scala

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ abstract class BlockHeaderValidatorSkeleton(blockchainConfig: BlockchainConfig)
2121

2222
import BlockHeaderValidator._
2323

24+
private val blockWithCheckpointHeaderValidator = new BlockWithCheckpointHeaderValidator(blockchainConfig)
25+
2426
/**
2527
* The difficulty calculator. This is specific to the consensus protocol.
2628
*/
@@ -39,6 +41,33 @@ abstract class BlockHeaderValidatorSkeleton(blockchainConfig: BlockchainConfig)
3941
* @param parentHeader BlockHeader of the parent of the block to validate.
4042
*/
4143
def validate(blockHeader: BlockHeader, parentHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
44+
if(blockHeader.hasCheckpoint) validateBlockWithCheckpointHeader(blockHeader, parentHeader)
45+
else validateRegularHeader(blockHeader, parentHeader)
46+
}
47+
48+
/** This method allows validate a BlockHeader (stated on
49+
* section 4.4.4 of http://paper.gavwood.com/).
50+
*
51+
* @param blockHeader BlockHeader to validate.
52+
* @param getBlockHeaderByHash function to obtain the parent.
53+
*/
54+
def validate(blockHeader: BlockHeader, getBlockHeaderByHash: GetBlockHeaderByHash): Either[BlockHeaderError, BlockHeaderValid] = {
55+
for {
56+
blockHeaderParent <- getBlockHeaderByHash(blockHeader.parentHash).map(Right(_)).getOrElse(Left(HeaderParentNotFoundError))
57+
_ <- validate(blockHeader, blockHeaderParent)
58+
} yield BlockHeaderValid
59+
}
60+
61+
/** This method runs a validation of the header of regular block.
62+
* It runs basic validation and pow validation (hidden in validateEvenMore)
63+
*
64+
* @param blockHeader BlockHeader to validate.
65+
* @param parentHeader BlockHeader of the parent of the block to validate.
66+
*/
67+
private def validateRegularHeader(
68+
blockHeader: BlockHeader,
69+
parentHeader: BlockHeader
70+
): Either[BlockHeaderError, BlockHeaderValid] = {
4271
for {
4372
// NOTE how we include everything except PoW (which is deferred to `validateEvenMore`),
4473
// and that difficulty validation is in effect abstract (due to `difficulty`).
@@ -53,16 +82,19 @@ abstract class BlockHeaderValidatorSkeleton(blockchainConfig: BlockchainConfig)
5382
} yield BlockHeaderValid
5483
}
5584

56-
/** This method allows validate a BlockHeader (stated on
57-
* section 4.4.4 of http://paper.gavwood.com/).
58-
*
59-
* @param blockHeader BlockHeader to validate.
60-
* @param getBlockHeaderByHash function to obtain the parent.
61-
*/
62-
def validate(blockHeader: BlockHeader, getBlockHeaderByHash: GetBlockHeaderByHash): Either[BlockHeaderError, BlockHeaderValid] = {
85+
/** This method runs a validation of the header of block with checkpoint.
86+
* It runs basic validation and checkpoint specific validation
87+
*
88+
* @param blockHeader BlockHeader to validate.
89+
* @param parentHeader BlockHeader of the parent of the block to validate.
90+
*/
91+
private def validateBlockWithCheckpointHeader(
92+
blockHeader: BlockHeader,
93+
parentHeader: BlockHeader
94+
): Either[BlockHeaderError, BlockHeaderValid] = {
6395
for {
64-
blockHeaderParent <- getBlockHeaderByHash(blockHeader.parentHash).map(Right(_)).getOrElse(Left(HeaderParentNotFoundError))
65-
_ <- validate(blockHeader, blockHeaderParent)
96+
_ <- blockWithCheckpointHeaderValidator.validate(blockHeader, parentHeader)
97+
_ <- validateNumber(blockHeader, parentHeader)
6698
} yield BlockHeaderValid
6799
}
68100

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package io.iohk.ethereum.consensus.validators
2+
3+
import io.iohk.ethereum.consensus.validators.BlockHeaderError._
4+
import io.iohk.ethereum.domain.BlockHeader
5+
import io.iohk.ethereum.ledger.BloomFilter
6+
import io.iohk.ethereum.utils.{BlockchainConfig, ByteStringUtils}
7+
8+
/** Validator specialized for the block with checkpoint
9+
*
10+
* @param blockchainConfig
11+
*/
12+
class BlockWithCheckpointHeaderValidator(blockchainConfig: BlockchainConfig) {
13+
14+
def validate(blockHeader: BlockHeader, parentHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
15+
for {
16+
_ <- validateECIP1097Number(blockHeader)
17+
_ <- validateCheckpointSignatures(blockHeader, parentHeader)
18+
_ <- validateEmptyFields(blockHeader)
19+
_ <- validateFieldsCopiedFromParent(blockHeader, parentHeader)
20+
_ <- validateGasUsed(blockHeader)
21+
_ <- validateTimestamp(blockHeader, parentHeader)
22+
} yield BlockHeaderValid
23+
}
24+
25+
/**
26+
* Validates [[io.iohk.ethereum.domain.BlockHeader.checkpoint]] signatures
27+
*
28+
* @param blockHeader BlockHeader to validate.
29+
* @param parentHeader BlockHeader of the parent of the block to validate.
30+
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderInvalidCheckpointSignatures]] otherwise
31+
*/
32+
private def validateCheckpointSignatures(
33+
blockHeader: BlockHeader,
34+
parentHeader: BlockHeader
35+
): Either[BlockHeaderError, BlockHeaderValid] = {
36+
blockHeader.checkpoint
37+
.map { checkpoint =>
38+
lazy val signaturesWithRecoveredKeys = checkpoint.signatures.map(s => s -> s.publicKey(parentHeader.hash))
39+
40+
// if at least 2 different signatures came from the same signer it will be in this set (also takes care
41+
// of duplicate signatures)
42+
lazy val repeatedSigners = signaturesWithRecoveredKeys
43+
.groupBy(_._2)
44+
.filter(_._2.size > 1)
45+
.keySet
46+
.flatten
47+
48+
lazy val (validSignatures, invalidSignatures) = signaturesWithRecoveredKeys.partition {
49+
//signatures are valid if the signers are known AND distinct
50+
case (sig, Some(pk)) => blockchainConfig.checkpointPubKeys.contains(pk) && !repeatedSigners.contains(pk)
51+
case _ => false
52+
}
53+
54+
// we fail fast if there are too many signatures (DoS protection)
55+
if (checkpoint.signatures.size > blockchainConfig.checkpointPubKeys.size)
56+
Left(HeaderWrongNumberOfCheckpointSignatures(checkpoint.signatures.size))
57+
else if (invalidSignatures.nonEmpty) {
58+
val sigsWithKeys = invalidSignatures.map {
59+
case (sig, maybePk) => (sig, maybePk.map(ByteStringUtils.hash2string))
60+
}
61+
Left(HeaderInvalidCheckpointSignatures(sigsWithKeys))
62+
} else if (validSignatures.size < blockchainConfig.minRequireSignatures)
63+
Left(HeaderWrongNumberOfCheckpointSignatures(validSignatures.size))
64+
else
65+
Right(BlockHeaderValid)
66+
}
67+
.getOrElse(Left(HeaderUnexpectedError("Attempted to validate a checkpoint on a block without a checkpoint")))
68+
}
69+
70+
/**
71+
* Validates emptiness of:
72+
* - beneficiary
73+
* - extraData
74+
* - treasuryOptOut
75+
* - ommersHash
76+
* - transactionsRoot
77+
* - receiptsRoot
78+
* - logsBloom
79+
* - nonce
80+
* - mixHash
81+
*
82+
* @param blockHeader BlockHeader to validate.
83+
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderFieldNotEmptyError]] otherwise
84+
*/
85+
private def validateEmptyFields(blockHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
86+
if (blockHeader.beneficiary != BlockHeader.EmptyBeneficiary)
87+
notEmptyFieldError("beneficiary")
88+
else if (blockHeader.ommersHash != BlockHeader.EmptyOmmers)
89+
notEmptyFieldError("ommersHash")
90+
else if (blockHeader.transactionsRoot != BlockHeader.EmptyMpt)
91+
notEmptyFieldError("transactionsRoot")
92+
else if (blockHeader.receiptsRoot != BlockHeader.EmptyMpt)
93+
notEmptyFieldError("receiptsRoot")
94+
else if (blockHeader.logsBloom != BloomFilter.EmptyBloomFilter)
95+
notEmptyFieldError("logsBloom")
96+
else if (blockHeader.extraData.nonEmpty)
97+
notEmptyFieldError("extraData")
98+
else if (blockHeader.treasuryOptOut.isDefined)
99+
notEmptyFieldError("treasuryOptOut")
100+
else if (blockHeader.nonce.nonEmpty)
101+
notEmptyFieldError("nonce")
102+
else if (blockHeader.mixHash.nonEmpty)
103+
notEmptyFieldError("mixHash")
104+
else Right(BlockHeaderValid)
105+
}
106+
107+
private def notEmptyFieldError(field: String) = Left(HeaderFieldNotEmptyError(s"$field is not empty"))
108+
109+
/**
110+
* Validates fields which should be equal to parent equivalents:
111+
* - stateRoot
112+
*
113+
* @param blockHeader BlockHeader to validate.
114+
* @param parentHeader BlockHeader of the parent of the block to validate.
115+
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderNotMatchParentError]] otherwise
116+
*/
117+
private def validateFieldsCopiedFromParent(
118+
blockHeader: BlockHeader,
119+
parentHeader: BlockHeader
120+
): Either[BlockHeaderError, BlockHeaderValid] = {
121+
if (blockHeader.stateRoot != parentHeader.stateRoot)
122+
fieldNotMatchedParentFieldError("stateRoot")
123+
else if (blockHeader.gasLimit != parentHeader.gasLimit)
124+
fieldNotMatchedParentFieldError("gasLimit")
125+
else if (blockHeader.difficulty != parentHeader.difficulty)
126+
fieldNotMatchedParentFieldError("difficulty")
127+
else Right(BlockHeaderValid)
128+
}
129+
130+
private def fieldNotMatchedParentFieldError(field: String) =
131+
Left(HeaderNotMatchParentError(s"$field has different value that similar parent field"))
132+
133+
134+
/**
135+
* Validates gasUsed equal to zero
136+
* @param blockHeader BlockHeader to validate.
137+
* @return BlockHeader if valid, an [[io.iohk.ethereum.consensus.validators.BlockHeaderError.HeaderGasUsedError]] otherwise
138+
*/
139+
private def validateGasUsed(blockHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
140+
if (blockHeader.gasUsed != BigInt(0)) Left(HeaderGasUsedError)
141+
else Right(BlockHeaderValid)
142+
}
143+
144+
/**
145+
* Validates [[io.iohk.ethereum.domain.BlockHeader.unixTimestamp]] is one bigger than parent unixTimestamp
146+
*
147+
* @param blockHeader BlockHeader to validate.
148+
* @param parentHeader BlockHeader of the parent of the block to validate.
149+
* @return BlockHeader if valid, an [[HeaderTimestampError]] otherwise
150+
*/
151+
private def validateTimestamp(
152+
blockHeader: BlockHeader,
153+
parentHeader: BlockHeader
154+
): Either[BlockHeaderError, BlockHeaderValid] =
155+
if (blockHeader.unixTimestamp == parentHeader.unixTimestamp + 1) Right(BlockHeaderValid)
156+
else Left(HeaderTimestampError)
157+
158+
/**
159+
* Validates [[io.iohk.ethereum.domain.BlockHeader.checkpoint]] is only defined if ECIP1097 is enabled at the block's number
160+
*
161+
* @param blockHeader BlockHeader to validate.
162+
* @return BlockHeader if valid, an [[HeaderCheckpointTooEarly]] otherwise
163+
*/
164+
private def validateECIP1097Number(blockHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
165+
if (blockHeader.number >= blockchainConfig.ecip1097BlockNumber) Right(BlockHeaderValid)
166+
else Left(HeaderCheckpointTooEarly)
167+
}
168+
}

0 commit comments

Comments
 (0)