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
1 change: 1 addition & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ mantis {
# If false then that 20% gets burned
# Doesn't have any effect is ecip1098 is not yet activated
treasury-opt-out = false

}

# This is the section dedicated to Ethash mining.
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/chains/etc-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,7 @@
"enode://715171f50508aba88aecd1250af392a45a330af91d7b90701c436b618c86aaa1589c9184561907bebbb56439b8f8787bc01f49a7c77276c58c1b09822d75e8e8@52.231.165.108:30303", // bootnode-azure-koreasouth-001
"enode://5d6d7cd20d6da4bb83a1d28cadb5d409b64edf314c0335df658c1a54e32c7c4a7ab7823d57c39b6a757556e68ff1df17c748b698544a55cb488b52479a92b60f@104.42.217.25:30303" // bootnode-azure-westus-001
]

# List of hex encoded public keys of Checkpoint Authorities
checkpoint-public-keys = []
}
3 changes: 3 additions & 0 deletions src/main/resources/chains/testnet-internal-chain.conf
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,7 @@

# Set of initial nodes
bootstrap-nodes = []

# List of hex encoded public keys of Checkpoint Authorities
checkpoint-public-keys = []
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package io.iohk.ethereum.consensus

import akka.util.ByteString
import com.typesafe.config.{Config TypesafeConfig}
import com.typesafe.config.{Config => TypesafeConfig}
import io.iohk.ethereum.consensus.validators.BlockHeaderValidator
import io.iohk.ethereum.domain.Address
import io.iohk.ethereum.nodebuilder.ShutdownHookBuilder
import io.iohk.ethereum.utils.Logger


/**
* Provides generic consensus configuration. Each consensus protocol implementation
* will use its own specific configuration as well.
Expand Down
21 changes: 21 additions & 0 deletions src/main/scala/io/iohk/ethereum/crypto/ECDSASignature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,24 @@ case class ECDSASignature(r: BigInt, s: BigInt, v: Byte) {
def publicKey(message: ByteString): Option[ByteString] =
ECDSASignature.recoverPubBytes(r, s, v, None, message.toArray[Byte]).map(ByteString(_))
}

object ECDSASignatureImplicits {

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

implicit val ecdsaSignatureDec: RLPDecoder[ECDSASignature] = new RLPDecoder[ECDSASignature] {
override def decode(rlp: RLPEncodeable): ECDSASignature = rlp match {
case RLPList(r, s, v) => ECDSASignature(r: ByteString, s: ByteString, v)
case _ => throw new RuntimeException("Cannot decode ECDSASignature")
}
}

implicit class ECDSASignatureEnc(ecdsaSignature: ECDSASignature) extends RLPSerializable {
override def toRLPEncodable: RLPEncodeable = {
RLPList(ecdsaSignature.r, ecdsaSignature.s, ecdsaSignature.v)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package io.iohk.ethereum.db.storage
import java.nio.ByteBuffer

import akka.util.ByteString
import boopickle.Default.{ Pickle, Unpickle }
import boopickle.Default.{Pickle, Unpickle}
import io.iohk.ethereum.crypto.ECDSASignature
import io.iohk.ethereum.db.dataSource.DataSource
import io.iohk.ethereum.db.storage.BlockBodiesStorage.BlockBodyHash
import io.iohk.ethereum.domain.{ Address, BlockHeader, BlockBody, SignedTransaction, Transaction }
import io.iohk.ethereum.domain.{Address, BlockBody, BlockHeader, Checkpoint, SignedTransaction, Transaction}
import io.iohk.ethereum.utils.ByteUtils.compactPickledBytes

/**
Expand Down Expand Up @@ -38,6 +38,7 @@ object BlockBodiesStorage {
transformPickler[Address, ByteString](bytes => Address(bytes))(address => address.bytes)
implicit val transactionPickler: Pickler[Transaction] = generatePickler[Transaction]
implicit val ecdsaSignaturePickler: Pickler[ECDSASignature] = generatePickler[ECDSASignature]
implicit val checkpointPickler: Pickler[Checkpoint] = generatePickler[Checkpoint]
implicit val signedTransactionPickler: Pickler[SignedTransaction] = transformPickler[SignedTransaction, (Transaction, ECDSASignature)]
{ case (tx, signature) => new SignedTransaction(tx, signature) }{ stx => (stx.tx, stx.signature)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package io.iohk.ethereum.db.storage
import java.nio.ByteBuffer

import akka.util.ByteString
import boopickle.Default.{ Pickle, Unpickle }
import boopickle.Default.{Pickle, Unpickle}
import io.iohk.ethereum.crypto.ECDSASignature
import io.iohk.ethereum.db.dataSource.DataSource
import io.iohk.ethereum.db.storage.BlockHeadersStorage.BlockHeaderHash
import io.iohk.ethereum.domain.BlockHeader
import io.iohk.ethereum.domain.{BlockHeader, Checkpoint}
import io.iohk.ethereum.utils.ByteUtils.compactPickledBytes

/**
Expand All @@ -31,53 +32,11 @@ class BlockHeadersStorage(val dataSource: DataSource) extends TransactionalKeyVa

object BlockHeadersStorage {
type BlockHeaderHash = ByteString
/** The following types are [[io.iohk.ethereum.domain.BlockHeader]] param types (in exact order).
*
* Mentioned params:
* parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot, logsBloom,
* difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce.
*/
type BlockHeaderBody = (
ByteString,
ByteString,
ByteString,
ByteString,
ByteString,
ByteString,
ByteString,
BigInt,
BigInt,
BigInt,
BigInt,
Long,
ByteString,
ByteString,
ByteString,
Option[Boolean]
)

import boopickle.DefaultBasic._

implicit val byteStringPickler: Pickler[ByteString] = transformPickler[ByteString, Array[Byte]](ByteString(_))(_.toArray[Byte])
implicit val blockHeaderPickler: Pickler[BlockHeader] = transformPickler[BlockHeader, BlockHeaderBody]
{ case (ph, oh, b, sr, txr, rr, lb, d, no, gl, gu, ut, ed, mh, n, oo) =>
new BlockHeader(ph, oh, b, sr, txr, rr, lb, d, no, gl, gu, ut, ed, mh, n, oo)
}{ blockHeader => (
blockHeader.parentHash,
blockHeader.ommersHash,
blockHeader.beneficiary,
blockHeader.stateRoot,
blockHeader.transactionsRoot,
blockHeader.receiptsRoot,
blockHeader.logsBloom,
blockHeader.difficulty,
blockHeader.number,
blockHeader.gasLimit,
blockHeader.gasUsed,
blockHeader.unixTimestamp,
blockHeader.extraData,
blockHeader.mixHash,
blockHeader.nonce,
blockHeader.treasuryOptOut
)}
implicit val ecdsaSignaturePickler: Pickler[ECDSASignature] = generatePickler[ECDSASignature]
implicit val checkpointPickler: Pickler[Checkpoint] = generatePickler[Checkpoint]
implicit val blockHeaderPickler: Pickler[BlockHeader] = generatePickler[BlockHeader]
}
2 changes: 1 addition & 1 deletion src/main/scala/io/iohk/ethereum/domain/BlockBody.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ object BlockBody {
rlpEncodableToBlockBody(
rlpEncodeable,
rlp => SignedTransactionRlpEncodableDec(rlp).toSignedTransaction,
rlp => BlockheaderEncodableDec(rlp).toBlockHeader
rlp => BlockHeaderDec(rlp).toBlockHeader
)

}
Expand Down
98 changes: 71 additions & 27 deletions src/main/scala/io/iohk/ethereum/domain/BlockHeader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package io.iohk.ethereum.domain

import akka.util.ByteString
import io.iohk.ethereum.crypto.kec256
import io.iohk.ethereum.rlp.RLPImplicitConversions._
import io.iohk.ethereum.rlp.RLPImplicits._
import io.iohk.ethereum.rlp.{RLPEncodeable, RLPList, RLPSerializable, rawDecode, encode => rlpEncode}
import io.iohk.ethereum.rlp.{RLPDecoder, RLPEncodeable, RLPEncoder, RLPList, RLPSerializable, rawDecode, encode => rlpEncode}
import org.bouncycastle.util.encoders.Hex

case class BlockHeader(
Expand All @@ -23,7 +21,8 @@ case class BlockHeader(
extraData: ByteString,
mixHash: ByteString,
nonce: ByteString,
treasuryOptOut: Option[Boolean]) {
treasuryOptOut: Option[Boolean],
checkpoint: Option[Checkpoint] = None) {

override def toString: String = {
s"""BlockHeader {
Expand All @@ -43,6 +42,7 @@ case class BlockHeader(
|mixHash: ${Hex.toHexString(mixHash.toArray[Byte])}
|nonce: ${Hex.toHexString(nonce.toArray[Byte])},
|treasuryOptOut: $treasuryOptOut
|withCheckpoint: ${checkpoint.isDefined}
|}""".stripMargin
}

Expand All @@ -54,72 +54,116 @@ case class BlockHeader(

lazy val hashAsHexString: String = Hex.toHexString(hash.toArray)

val hasCheckpoint: Boolean = checkpoint.isDefined

def idTag: String =
s"$number: $hashAsHexString"
}

object BlockHeader {

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

private implicit val checkpointOptionDecoder = implicitly[RLPDecoder[Option[Checkpoint]]]
private implicit val checkpointOptionEncoder = implicitly[RLPEncoder[Option[Checkpoint]]]

val emptyOmmerHash = ByteString(Hex.decode("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"))

def getEncodedWithoutNonce(blockHeader: BlockHeader): Array[Byte] = {
val rlpEncoded = blockHeader.toRLPEncodable match {
case rlpList: RLPList if blockHeader.treasuryOptOut.isEmpty =>
// Pre ECIP1098 block
RLPList(rlpList.items.dropRight(2): _*)
case rlpList: RLPList if blockHeader.checkpoint.isDefined =>
// post ECIP1098 & ECIP1097 block
val rlpItemsWithoutNonce = rlpList.items.dropRight(4) ++ rlpList.items.takeRight(2)
RLPList(rlpItemsWithoutNonce: _*)

case rlpList: RLPList if blockHeader.treasuryOptOut.isDefined =>
// Post ECIP1098 block
// Post ECIP1098 block without checkpoint
val rlpItemsWithoutNonce = rlpList.items.dropRight(3) :+ rlpList.items.last
RLPList(rlpItemsWithoutNonce: _*)

case rlpList: RLPList if blockHeader.treasuryOptOut.isEmpty =>
// Pre ECIP1098 & ECIP1097 block
RLPList(rlpList.items.dropRight(2): _*)

case _ => throw new Exception("BlockHeader cannot be encoded without nonce and mixHash")
}
rlpEncode(rlpEncoded)
}

implicit class BlockHeaderEnc(blockHeader: BlockHeader) extends RLPSerializable {
private def encodeOptOut(definedOptOut: Boolean) = {
val encodedOptOut = if(definedOptOut) 1 else 0
RLPList(encodedOptOut)
}
override def toRLPEncodable: RLPEncodeable = {
import blockHeader._
treasuryOptOut match {
case Some(definedOptOut) =>
// Post ECIP1098 block, whole block is encoded
val encodedOptOut = if(definedOptOut) 1 else 0
(treasuryOptOut, checkpoint) match {
case (Some(definedOptOut), Some(_)) =>
// Post ECIP1098 & ECIP1097 block, block with treasury enabled and checkpoint is encoded
RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, encodeOptOut(definedOptOut), checkpoint)

case (Some(definedOptOut), None) =>
// Post ECIP1098 block, Pre ECIP1097 or without checkpoint, block with treasury enabled is encoded
RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, RLPList(encodedOptOut))
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, encodeOptOut(definedOptOut))

case None =>
// Pre ECIP1098 block, encoding works as if optOut field wasn't defined for backwards compatibility
case (None, Some(_)) =>
// Post ECIP1097 block with checkpoint, treasury disabled, block with checkpoint is encoded
RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, RLPList(), checkpoint)
Copy link
Contributor

@rtkaczyk rtkaczyk Sep 25, 2020

Choose a reason for hiding this comment

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

It's a bit awkward. I wonder how other clients would deal with this - if they can easily switch encoding on a basis of block number then they might contest this design as unintuitive.

What if we had a single optional field interpreted as "block header extensions"? It would be always present after ECIP-X. Then we could explicitly state optionality of the new extensions in all cases, and it would be safe for any future extensions as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

do you mean one additional field for BlockHeader where we will pack all extensions ? I'm ok with that, but as it will touch also treasury opt, IMHO it should be done in different PR as it will add a lot of noise to the PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, definitely. This would be an important breaking change, so I'd like to consult that with others and plan it as proper a task.

I'm also not sure if this is the best we can do. @ntallar WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMHO it would be also good to do something similar in BlockchainConfig

Copy link

@ntallar ntallar Sep 25, 2020

Choose a reason for hiding this comment

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

Wouldn't a block header extensions field only move this complexity there, with the same matching on it's size as here? Why would that simplify any future extensions?

The only thing I think would simplify quite a lot the enconding/decoding design is having a block version field as with bitcoin (we could add it as the optional 17th field). Then:

  • Version 1: no version field, traditional ETC block
  • Version 2: treasuryOptOut as 18th field + checkpoint as 19th field
  • Future version 3: sth as 20th field, ...
  • ...

The spec didn't include that as I thought it might be an overkill, I haven't heard of future block headers changes for now

(I would also add any changes resulting from this discussion in a separate PR)

Copy link
Contributor

Choose a reason for hiding this comment

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

Why would that simplify any future extensions?

Not necessarily simplify, but we would always have explicitness regarding optionality. Anyway, I do like the version field idea better. I think this is the way to go 💯


case _ =>
// Pre ECIP1098 and ECIP1097 block, encoding works as if optOut and checkpoint fields weren't defined for backwards compatibility
RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce)
}
}
}

implicit class BlockheaderDec(val bytes: Array[Byte]) extends AnyVal {
def toBlockHeader: BlockHeader = BlockheaderEncodableDec(rawDecode(bytes)).toBlockHeader
implicit class BlockHeaderByteArrayDec(val bytes: Array[Byte]) extends AnyVal {
def toBlockHeader: BlockHeader = BlockHeaderDec(rawDecode(bytes)).toBlockHeader
}

implicit class BlockheaderEncodableDec(val rlpEncodeable: RLPEncodeable) extends AnyVal {
implicit class BlockHeaderDec(val rlpEncodeable: RLPEncodeable) extends AnyVal {
private def decodeOptOut(encodedOptOut: RLPEncodeable): Option[Boolean] = {
val booleanOptOut = {
if ((encodedOptOut: Int) == 1) true
else if ((encodedOptOut: Int) == 0) false
else throw new Exception("BlockHeader cannot be decoded with an invalid opt-out")
}
Some(booleanOptOut)
}
def toBlockHeader: BlockHeader = {
rlpEncodeable match {
case RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce) =>
// Pre ECIP1098 block, encoding works as if optOut field wasn't defined for backwards compatibility
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, RLPList(encodedOptOut), encodedCheckpoint) =>
// Post ECIP1098 & ECIP1097 block with checkpoint, whole block is encoded
BlockHeader(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce,
decodeOptOut(encodedOptOut), checkpointOptionDecoder.decode(encodedCheckpoint))

case RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, RLPList(), encodedCheckpoint) =>
// Post ECIP1098 & ECIP1097 block with checkpoint and treasury disabled
BlockHeader(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, None)
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, None, checkpointOptionDecoder.decode(encodedCheckpoint))


case RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, RLPList(encodedOptOut)) =>
// Post ECIP1098 block, whole block is encoded
val booleanOptOut =
if ((encodedOptOut: Int) == 1) true
else if ((encodedOptOut: Int) == 0) false
else throw new Exception("BlockHeader cannot be decoded with an invalid opt-out")
// Post ECIP1098 block without checkpoint
BlockHeader(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, decodeOptOut(encodedOptOut))


case RLPList(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce) =>
// Pre ECIP1098 and ECIP1097 block, decoding works as if optOut and checkpoint fields weren't defined for backwards compatibility
BlockHeader(parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot, receiptsRoot,
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, Some(booleanOptOut))
logsBloom, difficulty, number, gasLimit, gasUsed, unixTimestamp, extraData, mixHash, nonce, None, None)

case _ =>
throw new Exception("BlockHeader cannot be decoded")
Expand Down
25 changes: 25 additions & 0 deletions src/main/scala/io/iohk/ethereum/domain/Checkpoint.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.iohk.ethereum.domain

import io.iohk.ethereum.crypto.ECDSASignature
import io.iohk.ethereum.rlp._

case class Checkpoint(signatures: Seq[ECDSASignature])

object Checkpoint {

import io.iohk.ethereum.crypto.ECDSASignatureImplicits._

implicit val checkpointRLPEncoder: RLPEncoder[Checkpoint] = { checkpoint =>
RLPList(checkpoint.signatures.map(_.toRLPEncodable): _*)
}

implicit val checkpointRLPDecoder: RLPDecoder[Checkpoint] = {
case signatures: RLPList =>
Checkpoint(
signatures.items.map(ecdsaSignatureDec.decode)
)
case _ => throw new RuntimeException("Cannot decode Checkpoint")
}

def empty: Checkpoint = Checkpoint(Nil)
}
11 changes: 11 additions & 0 deletions src/main/scala/io/iohk/ethereum/rlp/RLPImplicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,15 @@ object RLPImplicits {
}
}

implicit def optionEnc[T](implicit enc: RLPEncoder[T]): RLPEncoder[Option[T]] = {
case None => RLPList()
case Some(value) => RLPList(enc.encode(value))
}

implicit def optionDec[T](implicit dec: RLPDecoder[T]): RLPDecoder[Option[T]] = {
Copy link

Choose a reason for hiding this comment

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

Maybe we should start using this for the optOut field eventually 🤔

Copy link
Contributor Author

@pslaski pslaski Sep 25, 2020

Choose a reason for hiding this comment

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

I think we can't as we are using custom Boolean encoding/decoding here.

Copy link

Choose a reason for hiding this comment

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

Is it custom? I copied it from how it's done on the reverse field of GetBlockHeaders

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't have any default one, but IMHO default Boolean could be true/false not 1/0 ;)

case RLPList(value) => Some(dec.decode(value))
case RLPList() => None
case rlp => throw RLPException(s"${rlp} should be a list with 1 or 0 elements")
}

}
Loading