diff --git a/src/main/resources/conf/chains/etc-chain.conf b/src/main/resources/conf/chains/etc-chain.conf index df654edd66..c6bacfca82 100644 --- a/src/main/resources/conf/chains/etc-chain.conf +++ b/src/main/resources/conf/chains/etc-chain.conf @@ -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. diff --git a/src/main/resources/conf/chains/eth-chain.conf b/src/main/resources/conf/chains/eth-chain.conf index fe00e198c4..a72c72ac06 100644 --- a/src/main/resources/conf/chains/eth-chain.conf +++ b/src/main/resources/conf/chains/eth-chain.conf @@ -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. @@ -292,4 +295,3 @@ "enode://979b7fa28feeb35a4741660a16076f1943202cb72b6af70d327f053e248bab9ba81760f39d0701ef1d8f89cc1fbd2cacba0710a12cd5314d5e0c9021aa3637f9@5.1.83.226:30303", ] } - diff --git a/src/main/resources/conf/chains/ropsten-chain.conf b/src/main/resources/conf/chains/ropsten-chain.conf index 6816ef5f9b..a372775b8f 100644 --- a/src/main/resources/conf/chains/ropsten-chain.conf +++ b/src/main/resources/conf/chains/ropsten-chain.conf @@ -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. diff --git a/src/main/resources/conf/chains/test-chain.conf b/src/main/resources/conf/chains/test-chain.conf index b6166249a3..9fdcb6293a 100644 --- a/src/main/resources/conf/chains/test-chain.conf +++ b/src/main/resources/conf/chains/test-chain.conf @@ -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. diff --git a/src/main/scala/io/iohk/ethereum/forkid/ForkId.scala b/src/main/scala/io/iohk/ethereum/forkid/ForkId.scala new file mode 100644 index 0000000000..6d80bbcd32 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/forkid/ForkId.scala @@ -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) + .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") + } + } +} diff --git a/src/main/scala/io/iohk/ethereum/utils/BlockchainConfig.scala b/src/main/scala/io/iohk/ethereum/utils/BlockchainConfig.scala index 275046dc83..07b2bc2133 100644 --- a/src/main/scala/io/iohk/ethereum/utils/BlockchainConfig.scala +++ b/src/main/scala/io/iohk/ethereum/utils/BlockchainConfig.scala @@ -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 { diff --git a/src/main/scala/io/iohk/ethereum/utils/Config.scala b/src/main/scala/io/iohk/ethereum/utils/Config.scala index 69880b21f8..4ec999da1a 100644 --- a/src/main/scala/io/iohk/ethereum/utils/Config.scala +++ b/src/main/scala/io/iohk/ethereum/utils/Config.scala @@ -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) @@ -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") } } } diff --git a/src/test/scala/io/iohk/ethereum/consensus/pow/validators/EthashBlockHeaderValidatorSpec.scala b/src/test/scala/io/iohk/ethereum/consensus/pow/validators/EthashBlockHeaderValidatorSpec.scala index eed395495f..aa42997db5 100644 --- a/src/test/scala/io/iohk/ethereum/consensus/pow/validators/EthashBlockHeaderValidatorSpec.scala +++ b/src/test/scala/io/iohk/ethereum/consensus/pow/validators/EthashBlockHeaderValidatorSpec.scala @@ -374,7 +374,6 @@ class EthashBlockHeaderValidatorSpec def createBlockchainConfig(supportsDaoFork: Boolean = false): BlockchainConfig = { import Fixtures.Blocks._ - BlockchainConfig( forkBlockNumbers = ForkBlockNumbers( frontierBlockNumber = 0, @@ -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, diff --git a/src/test/scala/io/iohk/ethereum/forkid/ForkIdSpec.scala b/src/test/scala/io/iohk/ethereum/forkid/ForkIdSpec.scala new file mode 100644 index 0000000000..f8b976e49e --- /dev/null +++ b/src/test/scala/io/iohk/ethereum/forkid/ForkIdSpec.scala @@ -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 + } +} diff --git a/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala b/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala index c076704a7b..f852918e40 100644 --- a/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala +++ b/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala @@ -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