-
Notifications
You must be signed in to change notification settings - Fork 78
[ETCM-70-71] Checkpointing domain + Checkpointing configuration #695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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( | ||
|
|
@@ -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 { | ||
|
|
@@ -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 | ||
| } | ||
|
|
||
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO it would be also good to do something similar in BlockchainConfig There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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") | ||
|
|
||
| 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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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]] = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should start using this for the optOut field eventually 🤔
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.