Skip to content

Commit 176884a

Browse files
Feature/etcm 354/forkid (#1018)
* [ETCM-354] Initialize the forkid package Implement gatherForks * [ETCM-354] Create ForkId using genesis hash and forks * [ETCM-354] Encode ForkId using RLP * [ETCM-354] Roundtrip ForkIds * [ETCM-354] Do not always use the DAO block for fork id validation * [ETCM-354] Print ForkId hash as hex string * [ETCM-354] Add ForkId calculation tests for Mordor * [ECIM-354] Apply review suggestions
1 parent c3abf45 commit 176884a

File tree

10 files changed

+216
-3
lines changed

10 files changed

+216
-3
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126

127127
# List of accounts to be drained
128128
drain-list = null
129+
130+
# Tells whether this fork should be included on the fork id list used for peer validation
131+
include-on-fork-id-list = false
129132
}
130133

131134
# Starting nonce of an empty account. Some networks (like Morden) use different values.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@
236236
"bb9bc244d798123fde783fcc1c72d3bb8c189413",
237237
"807640a13483f8ac783c557fcdf27be11ea4ac7a"
238238
]
239+
240+
# Tells whether this fork should be included on the fork id list used for peer validation
241+
include-on-fork-id-list = true
239242
}
240243

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@
122122

123123
# List of accounts to be drained
124124
drain-list = null
125+
126+
# Tells whether this fork should be included on the fork id list used for peer validation
127+
include-on-fork-id-list = true
125128
}
126129

127130
# Starting nonce of an empty account. Some networks (like Morden) use different values.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@
119119

120120
# List of accounts to be drained
121121
drain-list = null
122+
123+
# Tells whether this fork should be included on the fork id list used for peer validation
124+
include-on-fork-id-list = true
122125
}
123126

124127
# Starting nonce of an empty account. Some networks (like Morden) use different values.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.iohk.ethereum.forkid
2+
3+
import java.util.zip.CRC32
4+
import java.nio.ByteBuffer
5+
6+
import akka.util.ByteString
7+
import io.iohk.ethereum.utils.BlockchainConfig
8+
import io.iohk.ethereum.utils.BigIntExtensionMethods._
9+
import io.iohk.ethereum.utils.ByteUtils._
10+
import io.iohk.ethereum.utils.Hex
11+
import io.iohk.ethereum.rlp._
12+
13+
import RLPImplicitConversions._
14+
15+
case class ForkId(hash: BigInt, next: Option[BigInt]) {
16+
override def toString(): String = s"ForkId(0x${Hex.toHexString(hash.toUnsignedByteArray)}, $next)"
17+
}
18+
19+
object ForkId {
20+
21+
def create(genesisHash: ByteString, config: BlockchainConfig)(head: BigInt): ForkId = {
22+
val crc = new CRC32()
23+
crc.update(genesisHash.asByteBuffer)
24+
val next = gatherForks(config).find { fork =>
25+
if (fork <= head) {
26+
crc.update(bigIntToBytes(fork, 8))
27+
}
28+
fork > head
29+
}
30+
new ForkId(crc.getValue(), next)
31+
}
32+
33+
val noFork = BigInt("1000000000000000000")
34+
35+
def gatherForks(config: BlockchainConfig): List[BigInt] = {
36+
val maybeDaoBlock: Option[BigInt] = config.daoForkConfig.flatMap { daoConf =>
37+
if (daoConf.includeOnForkIdList) Some(daoConf.forkBlockNumber)
38+
else None
39+
}
40+
41+
(maybeDaoBlock.toList ++ config.forkBlockNumbers.all)
42+
.filterNot(v => v == 0 || v == noFork)
43+
.distinct
44+
.sorted
45+
}
46+
47+
implicit class ForkIdEnc(forkId: ForkId) extends RLPSerializable {
48+
import RLPImplicits._
49+
50+
import io.iohk.ethereum.utils.ByteUtils._
51+
override def toRLPEncodable: RLPEncodeable = {
52+
val hash: Array[Byte] = bigIntToBytes(forkId.hash, 4).takeRight(4)
53+
val next: Array[Byte] = bigIntToUnsignedByteArray(forkId.next.getOrElse(BigInt(0))).takeRight(8)
54+
RLPList(hash, next)
55+
}
56+
57+
}
58+
59+
implicit val forkIdEnc = new RLPDecoder[ForkId] {
60+
61+
def decode(rlp: RLPEncodeable): ForkId = rlp match {
62+
case RLPList(hash, next) => {
63+
val i = bigIntFromEncodeable(next)
64+
ForkId(bigIntFromEncodeable(hash), if (i == 0) None else Some(i))
65+
}
66+
case _ => throw new RuntimeException("Error when decoding ForkId")
67+
}
68+
}
69+
}

src/main/scala/io/iohk/ethereum/utils/BlockchainConfig.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,17 @@ case class ForkBlockNumbers(
5757
ecip1097BlockNumber: BigInt,
5858
ecip1049BlockNumber: Option[BigInt],
5959
ecip1099BlockNumber: BigInt
60-
)
60+
) {
61+
def all: List[BigInt] = this.productIterator.toList.flatMap {
62+
case i: BigInt => Some(i)
63+
case i: Option[_] =>
64+
i.flatMap {
65+
case n if n.isInstanceOf[BigInt] => Some(n.asInstanceOf[BigInt])
66+
case n => None
67+
}
68+
case default => None
69+
}
70+
}
6171

6272
object BlockchainConfig {
6373

src/main/scala/io/iohk/ethereum/utils/Config.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ trait DaoForkConfig {
304304
val range: Int
305305
val refundContract: Option[Address]
306306
val drainList: Seq[Address]
307+
val includeOnForkIdList: Boolean
307308

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

@@ -334,6 +335,7 @@ object DaoForkConfig {
334335
Try(daoConfig.getString("refund-contract-address")).toOption.map(Address(_))
335336
override val drainList: List[Address] =
336337
Try(daoConfig.getStringList("drain-list").asScala.toList).toOption.getOrElse(List.empty).map(Address(_))
338+
override val includeOnForkIdList: Boolean = daoConfig.getBoolean("include-on-fork-id-list")
337339
}
338340
}
339341
}

src/test/scala/io/iohk/ethereum/consensus/pow/validators/EthashBlockHeaderValidatorSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,6 @@ class EthashBlockHeaderValidatorSpec
374374

375375
def createBlockchainConfig(supportsDaoFork: Boolean = false): BlockchainConfig = {
376376
import Fixtures.Blocks._
377-
378377
BlockchainConfig(
379378
forkBlockNumbers = ForkBlockNumbers(
380379
frontierBlockNumber = 0,
@@ -408,6 +407,7 @@ class EthashBlockHeaderValidatorSpec
408407
if (supportsDaoFork) ProDaoForkBlock.header.hash else DaoForkBlock.header.hash
409408
override val forkBlockNumber: BigInt = DaoForkBlock.header.number
410409
override val refundContract: Option[Address] = None
410+
override val includeOnForkIdList: Boolean = false
411411
}),
412412
// unused
413413
maxCodeSize = None,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package io.iohk.ethereum.forkid
2+
3+
import akka.util.ByteString
4+
import io.iohk.ethereum.forkid.ForkId._
5+
import io.iohk.ethereum.utils.ForkBlockNumbers
6+
import io.iohk.ethereum.utils.Config._
7+
8+
import org.scalatest.wordspec.AnyWordSpec
9+
import org.scalatest.matchers.should._
10+
import org.bouncycastle.util.encoders.Hex
11+
12+
import io.iohk.ethereum.rlp._
13+
import io.iohk.ethereum.rlp.RLPImplicits._
14+
15+
16+
class ForkIdSpec extends AnyWordSpec with Matchers {
17+
18+
val config = blockchains
19+
20+
"ForkId" must {
21+
"gatherForks for all chain configurations without errors" in {
22+
config.blockchains.map { case (name, conf) => (name, gatherForks(conf)) }
23+
}
24+
"gatherForks for the etc chain correctly" in {
25+
val res = config.blockchains.map { case (name, conf) => (name, gatherForks(conf)) }
26+
res("etc") shouldBe List(1150000, 2500000, 3000000, 5000000, 5900000, 8772000, 9573000, 10500839, 11700000)
27+
}
28+
29+
"gatherForks for the eth chain correctly" in {
30+
val res = config.blockchains.map { case (name, conf) => (name, gatherForks(conf)) }
31+
res("eth") shouldBe List(1150000, 1920000, 2463000, 2675000, 4370000, 7280000, 9069000)
32+
}
33+
34+
"create correct ForkId for ETH mainnet blocks" in {
35+
val ethConf = config.blockchains("eth")
36+
val ethGenesisHash = ByteString(Hex.decode("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"))
37+
def create(head: BigInt) = ForkId.create(ethGenesisHash, ethConf)(head)
38+
39+
create(0) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Unsynced
40+
create(1149999) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Last Frontier block
41+
create(1150000) shouldBe ForkId(0x97c2c34cL, Some(1920000)) // First Homestead block
42+
create(1919999) shouldBe ForkId(0x97c2c34cL, Some(1920000)) // Last Homestead block
43+
create(1920000) shouldBe ForkId(0x91d1f948L, Some(2463000)) // First DAO block
44+
create(2462999) shouldBe ForkId(0x91d1f948L, Some(2463000)) // Last DAO block
45+
create(2463000) shouldBe ForkId(0x7a64da13L, Some(2675000)) // First Tangerine block
46+
create(2674999) shouldBe ForkId(0x7a64da13L, Some(2675000)) // Last Tangerine block
47+
create(2675000) shouldBe ForkId(0x3edd5b10L, Some(4370000)) // First Spurious block
48+
create(4369999) shouldBe ForkId(0x3edd5b10L, Some(4370000)) // Last Spurious block
49+
create(4370000) shouldBe ForkId(0xa00bc324L, Some(7280000)) // First Byzantium block
50+
create(7279999) shouldBe ForkId(0xa00bc324L, Some(7280000)) // Last Byzantium block
51+
create(7280000) shouldBe ForkId(0x668db0afL, Some(9069000)) // First and last Constantinople, first Petersburg block
52+
create(9068999) shouldBe ForkId(0x668db0afL, Some(9069000)) // Last Petersburg block
53+
// TODO: Add Muir Glacier and Berlin
54+
create(9069000) shouldBe ForkId(0x879d6e30L, None) // First Istanbul block
55+
create(12644529) shouldBe ForkId(0x879d6e30L, None) // Today Istanbul block
56+
}
57+
58+
"create correct ForkId for ETC mainnet blocks" in {
59+
val etcConf = config.blockchains("etc")
60+
val etcGenesisHash = ByteString(Hex.decode("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"))
61+
def create(head: BigInt) = ForkId.create(etcGenesisHash, etcConf)(head)
62+
63+
create(0) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Unsynced
64+
create(1149999) shouldBe ForkId(0xfc64ec04L, Some(1150000)) // Last Frontier block
65+
create(1150000) shouldBe ForkId(0x97c2c34cL, Some(2500000)) // First Homestead block
66+
create(1919999) shouldBe ForkId(0x97c2c34cL, Some(2500000)) // Last Homestead block
67+
create(2500000) shouldBe ForkId(0xdb06803fL, Some(3000000))
68+
create(3000000-1) shouldBe ForkId(0xdb06803fL, Some(3000000))
69+
create(3000000) shouldBe ForkId(0xaff4bed4L, Some(5000000))
70+
create(5000000-1) shouldBe ForkId(0xaff4bed4L, Some(5000000))
71+
create(5000000) shouldBe ForkId(0xf79a63c0L, Some(5900000))
72+
create(5900000-1) shouldBe ForkId(0xf79a63c0L, Some(5900000))
73+
create(5900000) shouldBe ForkId(0x744899d6L, Some(8772000))
74+
create(8772000-1) shouldBe ForkId(0x744899d6L, Some(8772000))
75+
create(8772000) shouldBe ForkId(0x518b59c6L, Some(9573000))
76+
create(9573000-1) shouldBe ForkId(0x518b59c6L, Some(9573000))
77+
create(9573000) shouldBe ForkId(0x7ba22882L, Some(10500839))
78+
create(10500839-1) shouldBe ForkId(0x7ba22882L, Some(10500839))
79+
create(10500839) shouldBe ForkId(0x9007bfccL, Some(11700000))
80+
create(11700000-1) shouldBe ForkId(0x9007bfccL, Some(11700000))
81+
create(11700000) shouldBe ForkId(0xdb63a1caL, None)
82+
}
83+
84+
"create correct ForkId for mordor blocks" in {
85+
val mordorConf = config.blockchains("mordor")
86+
val mordorGenesisHash = ByteString(Hex.decode("a68ebde7932eccb177d38d55dcc6461a019dd795a681e59b5a3e4f3a7259a3f1"))
87+
def create(head: BigInt) = ForkId.create(mordorGenesisHash, mordorConf)(head)
88+
89+
create(0) shouldBe ForkId(0x175782aaL, Some(301243)) // Unsynced
90+
create(301242) shouldBe ForkId(0x175782aaL, Some(301243))
91+
create(301243) shouldBe ForkId(0x604f6ee1L, Some(999983))
92+
create(999982) shouldBe ForkId(0x604f6ee1L, Some(999983))
93+
create(999983) shouldBe ForkId(0xf42f5539L, Some(2520000))
94+
create(2519999) shouldBe ForkId(0xf42f5539L, Some(2520000))
95+
create(2520000) shouldBe ForkId(0x66b5c286L, None)
96+
// TODO: Add Magneto
97+
// create(2520000) shouldBe ForkId(0x66b5c286L, Some(3985893))
98+
// create(3985893) shouldBe ForkId(0x66b5c286L, Some(3985893))
99+
// create(3985894) shouldBe ForkId(0x92b323e0L, None)
100+
}
101+
102+
// 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):
103+
"be correctly encoded via rlp" in {
104+
roundTrip(ForkId(0, None), "c6840000000080")
105+
roundTrip(ForkId(0xdeadbeefL, Some(0xBADDCAFEL)), "ca84deadbeef84baddcafe")
106+
107+
val maxUInt64 = (BigInt(0x7FFFFFFFFFFFFFFFL) << 1) + 1
108+
maxUInt64.toByteArray shouldBe Array(0, -1, -1, -1, -1, -1, -1, -1, -1)
109+
val maxUInt32 = BigInt(0xFFFFFFFFL)
110+
maxUInt32.toByteArray shouldBe Array(0, -1, -1, -1, -1)
111+
112+
roundTrip(ForkId(maxUInt32, Some(maxUInt64)), "ce84ffffffff88ffffffffffffffff")
113+
}
114+
}
115+
116+
private def roundTrip(forkId: ForkId, hex: String) = {
117+
encode(forkId.toRLPEncodable) shouldBe Hex.decode(hex)
118+
decode[ForkId](Hex.decode(hex)) shouldBe forkId
119+
}
120+
}

src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ trait DaoForkTestSetup extends TestSetup with MockFactory {
204204
override val forkBlockHash: ByteString = proDaoBlock.header.hash
205205
override val forkBlockNumber: BigInt = proDaoBlock.header.number
206206
override val refundContract: Option[Address] = Some(Address(4))
207+
override val includeOnForkIdList: Boolean = false
207208
}
208209

209210
val proDaoBlockchainConfig: BlockchainConfig = blockchainConfig

0 commit comments

Comments
 (0)