Skip to content
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
3 changes: 3 additions & 0 deletions src/main/resources/conf/chains/etc-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@

# List of accounts to be drained
drain-list = null

# Tells whether this fork should be included on the fork id list used for peer validation
include-on-fork-id-list = false
}

# Starting nonce of an empty account. Some networks (like Morden) use different values.
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/conf/chains/eth-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@
"bb9bc244d798123fde783fcc1c72d3bb8c189413",
"807640a13483f8ac783c557fcdf27be11ea4ac7a"
]

# Tells whether this fork should be included on the fork id list used for peer validation
include-on-fork-id-list = true
}

# Starting nonce of an empty account. Some networks (like Morden) use different values.
Expand Down Expand Up @@ -292,4 +295,3 @@
"enode://979b7fa28feeb35a4741660a16076f1943202cb72b6af70d327f053e248bab9ba81760f39d0701ef1d8f89cc1fbd2cacba0710a12cd5314d5e0c9021aa3637f9@5.1.83.226:30303",
]
}

3 changes: 3 additions & 0 deletions src/main/resources/conf/chains/ropsten-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@

# List of accounts to be drained
drain-list = null

# Tells whether this fork should be included on the fork id list used for peer validation
include-on-fork-id-list = true
}

# Starting nonce of an empty account. Some networks (like Morden) use different values.
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/conf/chains/test-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@

# List of accounts to be drained
drain-list = null

# Tells whether this fork should be included on the fork id list used for peer validation
include-on-fork-id-list = true
}

# Starting nonce of an empty account. Some networks (like Morden) use different values.
Expand Down
69 changes: 69 additions & 0 deletions src/main/scala/io/iohk/ethereum/forkid/ForkId.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.iohk.ethereum.forkid

import java.util.zip.CRC32
import java.nio.ByteBuffer

import akka.util.ByteString
import io.iohk.ethereum.utils.BlockchainConfig
import io.iohk.ethereum.utils.BigIntExtensionMethods._
import io.iohk.ethereum.utils.ByteUtils._
import io.iohk.ethereum.utils.Hex
import io.iohk.ethereum.rlp._

import RLPImplicitConversions._

case class ForkId(hash: BigInt, next: Option[BigInt]) {
override def toString(): String = s"ForkId(0x${Hex.toHexString(hash.toUnsignedByteArray)}, $next)"
}

object ForkId {

def create(genesisHash: ByteString, config: BlockchainConfig)(head: BigInt): ForkId = {
val crc = new CRC32()
crc.update(genesisHash.asByteBuffer)
val next = gatherForks(config).find { fork =>
if (fork <= head) {
crc.update(bigIntToBytes(fork, 8))
}
fork > head
}
new ForkId(crc.getValue(), next)
}

val noFork = BigInt("1000000000000000000")

def gatherForks(config: BlockchainConfig): List[BigInt] = {
val maybeDaoBlock: Option[BigInt] = config.daoForkConfig.flatMap { daoConf =>
if (daoConf.includeOnForkIdList) Some(daoConf.forkBlockNumber)
else None
}

(maybeDaoBlock.toList ++ config.forkBlockNumbers.all)
.filterNot(v => v == 0 || v == noFork)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why we filter out noFork. It does not seem to be used somewhere else

Copy link
Contributor Author

@lukasz-golebiewski lukasz-golebiewski Jun 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same configuration structure BlockchainConfig is re-used for multiple chains. Forks which shouldn't occur in a given chain are configured to happen at block height "1000000000000000000" which is equivalent to "never". We don't want those fork numbers on our fork id list

.distinct
.sorted
}

implicit class ForkIdEnc(forkId: ForkId) extends RLPSerializable {
import RLPImplicits._

import io.iohk.ethereum.utils.ByteUtils._
override def toRLPEncodable: RLPEncodeable = {
val hash: Array[Byte] = bigIntToBytes(forkId.hash, 4).takeRight(4)
val next: Array[Byte] = bigIntToUnsignedByteArray(forkId.next.getOrElse(BigInt(0))).takeRight(8)
RLPList(hash, next)
}

}

implicit val forkIdEnc = new RLPDecoder[ForkId] {

def decode(rlp: RLPEncodeable): ForkId = rlp match {
case RLPList(hash, next) => {
val i = bigIntFromEncodeable(next)
ForkId(bigIntFromEncodeable(hash), if (i == 0) None else Some(i))
}
case _ => throw new RuntimeException("Error when decoding ForkId")
}
}
}
12 changes: 11 additions & 1 deletion src/main/scala/io/iohk/ethereum/utils/BlockchainConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@ case class ForkBlockNumbers(
ecip1097BlockNumber: BigInt,
ecip1049BlockNumber: Option[BigInt],
ecip1099BlockNumber: BigInt
)
) {
def all: List[BigInt] = this.productIterator.toList.flatMap {
case i: BigInt => Some(i)
case i: Option[_] =>
i.flatMap {
case n if n.isInstanceOf[BigInt] => Some(n.asInstanceOf[BigInt])
case n => None
}
case default => None
}
}

object BlockchainConfig {

Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/io/iohk/ethereum/utils/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ trait DaoForkConfig {
val range: Int
val refundContract: Option[Address]
val drainList: Seq[Address]
val includeOnForkIdList: Boolean

private lazy val extratadaBlockRange = forkBlockNumber until (forkBlockNumber + range)

Expand Down Expand Up @@ -334,6 +335,7 @@ object DaoForkConfig {
Try(daoConfig.getString("refund-contract-address")).toOption.map(Address(_))
override val drainList: List[Address] =
Try(daoConfig.getStringList("drain-list").asScala.toList).toOption.getOrElse(List.empty).map(Address(_))
override val includeOnForkIdList: Boolean = daoConfig.getBoolean("include-on-fork-id-list")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,6 @@ class EthashBlockHeaderValidatorSpec

def createBlockchainConfig(supportsDaoFork: Boolean = false): BlockchainConfig = {
import Fixtures.Blocks._

BlockchainConfig(
forkBlockNumbers = ForkBlockNumbers(
frontierBlockNumber = 0,
Expand Down Expand Up @@ -408,6 +407,7 @@ class EthashBlockHeaderValidatorSpec
if (supportsDaoFork) ProDaoForkBlock.header.hash else DaoForkBlock.header.hash
override val forkBlockNumber: BigInt = DaoForkBlock.header.number
override val refundContract: Option[Address] = None
override val includeOnForkIdList: Boolean = false
}),
// unused
maxCodeSize = None,
Expand Down
120 changes: 120 additions & 0 deletions src/test/scala/io/iohk/ethereum/forkid/ForkIdSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package io.iohk.ethereum.forkid

import akka.util.ByteString
import io.iohk.ethereum.forkid.ForkId._
import io.iohk.ethereum.utils.ForkBlockNumbers
import io.iohk.ethereum.utils.Config._

import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should._
import org.bouncycastle.util.encoders.Hex

import io.iohk.ethereum.rlp._
import io.iohk.ethereum.rlp.RLPImplicits._


class ForkIdSpec extends AnyWordSpec with Matchers {

val config = blockchains

"ForkId" must {
"gatherForks for all chain configurations without errors" in {
config.blockchains.map { case (name, conf) => (name, gatherForks(conf)) }
}
"gatherForks for the etc chain correctly" in {
val res = config.blockchains.map { case (name, conf) => (name, gatherForks(conf)) }
res("etc") shouldBe List(1150000, 2500000, 3000000, 5000000, 5900000, 8772000, 9573000, 10500839, 11700000)
}

"gatherForks for the eth chain correctly" in {
val res = config.blockchains.map { case (name, conf) => (name, gatherForks(conf)) }
res("eth") shouldBe List(1150000, 1920000, 2463000, 2675000, 4370000, 7280000, 9069000)
}

"create correct ForkId for ETH mainnet blocks" in {
val ethConf = config.blockchains("eth")
val ethGenesisHash = ByteString(Hex.decode("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"))
def create(head: BigInt) = ForkId.create(ethGenesisHash, ethConf)(head)

create(0) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Unsynced
create(1149999) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Last Frontier block
create(1150000) shouldBe ForkId(0x97c2c34cL, Some(1920000)) // First Homestead block
create(1919999) shouldBe ForkId(0x97c2c34cL, Some(1920000)) // Last Homestead block
create(1920000) shouldBe ForkId(0x91d1f948L, Some(2463000)) // First DAO block
create(2462999) shouldBe ForkId(0x91d1f948L, Some(2463000)) // Last DAO block
create(2463000) shouldBe ForkId(0x7a64da13L, Some(2675000)) // First Tangerine block
create(2674999) shouldBe ForkId(0x7a64da13L, Some(2675000)) // Last Tangerine block
create(2675000) shouldBe ForkId(0x3edd5b10L, Some(4370000)) // First Spurious block
create(4369999) shouldBe ForkId(0x3edd5b10L, Some(4370000)) // Last Spurious block
create(4370000) shouldBe ForkId(0xa00bc324L, Some(7280000)) // First Byzantium block
create(7279999) shouldBe ForkId(0xa00bc324L, Some(7280000)) // Last Byzantium block
create(7280000) shouldBe ForkId(0x668db0afL, Some(9069000)) // First and last Constantinople, first Petersburg block
create(9068999) shouldBe ForkId(0x668db0afL, Some(9069000)) // Last Petersburg block
// TODO: Add Muir Glacier and Berlin
create(9069000) shouldBe ForkId(0x879d6e30L, None) // First Istanbul block
create(12644529) shouldBe ForkId(0x879d6e30L, None) // Today Istanbul block
}

"create correct ForkId for ETC mainnet blocks" in {
val etcConf = config.blockchains("etc")
val etcGenesisHash = ByteString(Hex.decode("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"))
def create(head: BigInt) = ForkId.create(etcGenesisHash, etcConf)(head)

create(0) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Unsynced
create(1149999) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Last Frontier block
create(1150000) shouldBe ForkId(0x97c2c34cL, Some(2500000)) // First Homestead block
create(1919999) shouldBe ForkId(0x97c2c34cL, Some(2500000)) // Last Homestead block
create(2500000) shouldBe ForkId(0xdb06803fL, Some(3000000))
create(3000000-1) shouldBe ForkId(0xdb06803fL, Some(3000000))
create(3000000) shouldBe ForkId(0xaff4bed4L, Some(5000000))
create(5000000-1) shouldBe ForkId(0xaff4bed4L, Some(5000000))
create(5000000) shouldBe ForkId(0xf79a63c0L, Some(5900000))
create(5900000-1) shouldBe ForkId(0xf79a63c0L, Some(5900000))
create(5900000) shouldBe ForkId(0x744899d6L, Some(8772000))
create(8772000-1) shouldBe ForkId(0x744899d6L, Some(8772000))
create(8772000) shouldBe ForkId(0x518b59c6L, Some(9573000))
create(9573000-1) shouldBe ForkId(0x518b59c6L, Some(9573000))
create(9573000) shouldBe ForkId(0x7ba22882L, Some(10500839))
create(10500839-1) shouldBe ForkId(0x7ba22882L, Some(10500839))
create(10500839) shouldBe ForkId(0x9007bfccL, Some(11700000))
create(11700000-1) shouldBe ForkId(0x9007bfccL, Some(11700000))
create(11700000) shouldBe ForkId(0xdb63a1caL, None)
}

"create correct ForkId for mordor blocks" in {
val mordorConf = config.blockchains("mordor")
val mordorGenesisHash = ByteString(Hex.decode("a68ebde7932eccb177d38d55dcc6461a019dd795a681e59b5a3e4f3a7259a3f1"))
def create(head: BigInt) = ForkId.create(mordorGenesisHash, mordorConf)(head)

create(0) shouldBe ForkId(0x175782aaL, Some(301243)) // Unsynced
create(301242) shouldBe ForkId(0x175782aaL, Some(301243))
create(301243) shouldBe ForkId(0x604f6ee1L, Some(999983))
create(999982) shouldBe ForkId(0x604f6ee1L, Some(999983))
create(999983) shouldBe ForkId(0xf42f5539L, Some(2520000))
create(2519999) shouldBe ForkId(0xf42f5539L, Some(2520000))
create(2520000) shouldBe ForkId(0x66b5c286L, None)
// TODO: Add Magneto
// create(2520000) shouldBe ForkId(0x66b5c286L, Some(3985893))
// create(3985893) shouldBe ForkId(0x66b5c286L, Some(3985893))
// create(3985894) shouldBe ForkId(0x92b323e0L, None)
}

// Here’s a couple of tests to verify the proper RLP encoding (since FORK_HASH is a 4 byte binary but FORK_NEXT is an 8 byte quantity):
"be correctly encoded via rlp" in {
roundTrip(ForkId(0, None), "c6840000000080")
roundTrip(ForkId(0xdeadbeefL, Some(0xBADDCAFEL)), "ca84deadbeef84baddcafe")

val maxUInt64 = (BigInt(0x7FFFFFFFFFFFFFFFL) << 1) + 1
maxUInt64.toByteArray shouldBe Array(0, -1, -1, -1, -1, -1, -1, -1, -1)
val maxUInt32 = BigInt(0xFFFFFFFFL)
maxUInt32.toByteArray shouldBe Array(0, -1, -1, -1, -1)

roundTrip(ForkId(maxUInt32, Some(maxUInt64)), "ce84ffffffff88ffffffffffffffff")
}
}

private def roundTrip(forkId: ForkId, hex: String) = {
encode(forkId.toRLPEncodable) shouldBe Hex.decode(hex)
decode[ForkId](Hex.decode(hex)) shouldBe forkId
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ trait DaoForkTestSetup extends TestSetup with MockFactory {
override val forkBlockHash: ByteString = proDaoBlock.header.hash
override val forkBlockNumber: BigInt = proDaoBlock.header.number
override val refundContract: Option[Address] = Some(Address(4))
override val includeOnForkIdList: Boolean = false
}

val proDaoBlockchainConfig: BlockchainConfig = blockchainConfig
Expand Down