Skip to content

Feature/etcm 354/forkid #1018

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 8 commits into from
Jun 22, 2021
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