Skip to content

Commit cd5ae33

Browse files
authored
Merge pull request #738 from input-output-hk/etcm-174-validate-lexicographically-signatures
[ETCM-174] Sorting of checkpoint signatures
2 parents 65b93ad + c515106 commit cd5ae33

File tree

6 files changed

+55
-11
lines changed

6 files changed

+55
-11
lines changed

src/main/scala/io/iohk/ethereum/consensus/blocks/CheckpointBlockGenerator.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.iohk.ethereum.consensus.blocks
22

33
import akka.util.ByteString
4+
import io.iohk.ethereum.crypto.ECDSASignatureImplicits.ECDSASignatureOrdering
45
import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefPostEcip1097
56
import io.iohk.ethereum.domain._
67
import io.iohk.ethereum.ledger.BloomFilter
@@ -12,6 +13,7 @@ class CheckpointBlockGenerator {
1213
// we are using a predictable value for timestamp so that each federation node generates identical block
1314
// see ETCM-173
1415
val timestamp = parent.header.unixTimestamp + 1
16+
val checkpointWithSortedSignatures = Checkpoint(checkpoint.signatures.sorted)
1517

1618
val header = BlockHeader(
1719
parentHash = parent.hash,
@@ -29,7 +31,7 @@ class CheckpointBlockGenerator {
2931
gasUsed = UInt256.Zero,
3032
mixHash = ByteString.empty,
3133
nonce = ByteString.empty,
32-
extraFields = HefPostEcip1097(false, Some(checkpoint))
34+
extraFields = HefPostEcip1097(false, Some(checkpointWithSortedSignatures))
3335
)
3436

3537
Block(header, BlockBody.empty)

src/main/scala/io/iohk/ethereum/consensus/validators/BlockHeaderValidator.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ object BlockHeaderError {
4242
case class HeaderWrongNumberOfCheckpointSignatures(sigCount: Int) extends BlockHeaderError
4343
case class HeaderInvalidCheckpointSignatures(invalidSignaturesWithPublics: Seq[(ECDSASignature, Option[String])])
4444
extends BlockHeaderError
45+
case object HeaderInvalidOrderOfCheckpointSignatures extends BlockHeaderError
4546
case class HeaderFieldNotEmptyError(msg: String) extends BlockHeaderError
4647
case class HeaderNotMatchParentError(msg: String) extends BlockHeaderError
4748
case object CheckpointHeaderTreasuryOptOutError extends BlockHeaderError

src/main/scala/io/iohk/ethereum/consensus/validators/BlockWithCheckpointHeaderValidator.scala

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class BlockWithCheckpointHeaderValidator(blockchainConfig: BlockchainConfig) {
1313

1414
def validate(blockHeader: BlockHeader, parentHeader: BlockHeader): Either[BlockHeaderError, BlockHeaderValid] = {
1515
for {
16+
_ <- validateLexicographicalOrderOfSignatures(blockHeader)
1617
_ <- validateCheckpointSignatures(blockHeader, parentHeader)
1718
_ <- validateEmptyFields(blockHeader)
1819
_ <- validateFieldsCopiedFromParent(blockHeader, parentHeader)
@@ -22,6 +23,19 @@ class BlockWithCheckpointHeaderValidator(blockchainConfig: BlockchainConfig) {
2223
} yield BlockHeaderValid
2324
}
2425

26+
private def validateLexicographicalOrderOfSignatures(
27+
header: BlockHeader
28+
): Either[BlockHeaderError, BlockHeaderValid] = {
29+
import io.iohk.ethereum.crypto.ECDSASignatureImplicits.ECDSASignatureOrdering
30+
header.checkpoint
31+
.map { checkpoint =>
32+
if (checkpoint.signatures == checkpoint.signatures.sorted) {
33+
Right(BlockHeaderValid)
34+
} else Left(HeaderInvalidOrderOfCheckpointSignatures)
35+
}
36+
.getOrElse(Left(BlockWithCheckpointHeaderValidator.NoCheckpointInHeaderError))
37+
}
38+
2539
/**
2640
* Validates [[io.iohk.ethereum.domain.BlockHeader.checkpoint]] signatures
2741
*
@@ -64,7 +78,7 @@ class BlockWithCheckpointHeaderValidator(blockchainConfig: BlockchainConfig) {
6478
else
6579
Right(BlockHeaderValid)
6680
}
67-
.getOrElse(Left(HeaderUnexpectedError("Attempted to validate a checkpoint on a block without a checkpoint")))
81+
.getOrElse(Left(BlockWithCheckpointHeaderValidator.NoCheckpointInHeaderError))
6882
}
6983

7084
/**
@@ -157,3 +171,9 @@ class BlockWithCheckpointHeaderValidator(blockchainConfig: BlockchainConfig) {
157171
else Left(CheckpointHeaderTreasuryOptOutError)
158172

159173
}
174+
175+
object BlockWithCheckpointHeaderValidator {
176+
val NoCheckpointInHeaderError: BlockHeaderError = HeaderUnexpectedError(
177+
"Attempted to validate a checkpoint on a block without a checkpoint"
178+
)
179+
}

src/main/scala/io/iohk/ethereum/crypto/ECDSASignature.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,5 @@ object ECDSASignatureImplicits {
183183
}
184184
}
185185

186+
implicit val ECDSASignatureOrdering: Ordering[ECDSASignature] = Ordering.by(sig => (sig.r, sig.s, sig.v))
186187
}

src/test/scala/io/iohk/ethereum/checkpointing/CheckpointingTestHelpers.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefPostEcip1097
66
import io.iohk.ethereum.domain._
77
import io.iohk.ethereum.ledger.BloomFilter
88
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
9+
import io.iohk.ethereum.crypto.ECDSASignatureImplicits.ECDSASignatureOrdering
910

1011
object CheckpointingTestHelpers {
1112
def createBlockWithCheckpoint(
@@ -45,5 +46,5 @@ object CheckpointingTestHelpers {
4546
): Seq[ECDSASignature] =
4647
keys.map { k =>
4748
ECDSASignature.sign(hash.toArray, k)
48-
}
49+
}.sorted
4950
}

src/test/scala/io/iohk/ethereum/consensus/validators/BlockWithCheckpointHeaderValidatorSpec.scala

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
66
import io.iohk.ethereum.consensus.difficulty.DifficultyCalculator
77
import io.iohk.ethereum.consensus.validators.BlockHeaderError._
88
import io.iohk.ethereum.crypto.ECDSASignature
9+
import io.iohk.ethereum.crypto.ECDSASignatureImplicits.ECDSASignatureOrdering
910
import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefPostEcip1097
1011
import io.iohk.ethereum.domain._
1112
import io.iohk.ethereum.ledger.BloomFilter
@@ -155,6 +156,18 @@ class BlockWithCheckpointHeaderValidatorSpec
155156
)
156157
}
157158

159+
it should "return failure when checkpoint signatures aren't sorted lexicographically" in new TestSetup {
160+
val invalidBlockHeaderExtraFields = HefPostEcip1097(
161+
false,
162+
Some(Checkpoint(validCheckpoint.signatures.reverse))
163+
)
164+
val invalidBlockHeader =
165+
validBlockHeaderWithCheckpoint.copy(extraFields = invalidBlockHeaderExtraFields)
166+
blockHeaderValidator.validate(invalidBlockHeader, validBlockParentHeader) shouldBe Left(
167+
HeaderInvalidOrderOfCheckpointSignatures
168+
)
169+
}
170+
158171
it should "return failure when checkpoint has not enough valid signatures" in new TestSetup {
159172
val invalidBlockHeaderExtraFields = HefPostEcip1097(
160173
false,
@@ -170,10 +183,11 @@ class BlockWithCheckpointHeaderValidatorSpec
170183
it should "return failure when checkpoint has enough valid signatures, but also an invalid one" in new TestSetup {
171184
val invalidKeys = crypto.generateKeyPair(secureRandom)
172185
val invalidSignatures =
173-
CheckpointingTestHelpers.createCheckpointSignatures(Seq(invalidKeys), validBlockParentHeader.hash)
186+
CheckpointingTestHelpers.createCheckpointSignatures(Seq(invalidKeys), validBlockParent.hash)
187+
val signatures = invalidSignatures ++ validCheckpoint.signatures
174188
val invalidBlockHeaderExtraFields = HefPostEcip1097(
175189
false,
176-
Some(Checkpoint(invalidSignatures ++ validCheckpoint.signatures))
190+
Some(Checkpoint(signatures.sorted))
177191
)
178192
val invalidBlockHeader = validBlockHeaderWithCheckpoint.copy(extraFields = invalidBlockHeaderExtraFields)
179193
blockHeaderValidator.validate(invalidBlockHeader, validBlockParentHeader) shouldBe Left(
@@ -203,11 +217,15 @@ class BlockWithCheckpointHeaderValidatorSpec
203217
"7e1573bc593f289793304c50fa8068d35f8611e5c558337c72b6bcfef1dbfc884226ad305a97659fc172d347b70ea7bfca011859118efcee33f3b5e02d31c3cd1b"
204218
val sameSignerSig = ECDSASignature.fromBytes(ByteStringUtils.string2hash(sameSignerSigHex)).get
205219

206-
val invalidCheckpoint = Checkpoint(sameSignerSig +: validCheckpoint.signatures)
220+
val invalidCheckpoint = Checkpoint((sameSignerSig +: validCheckpoint.signatures).sorted)
207221

208222
// verify that we have 2 signatures from the same signer
209-
val actualSigners = invalidCheckpoint.signatures.flatMap(_.publicKey(validBlockParentHeader.hash))
210-
val expectedSigners = (keys.head +: keys).map(kp => ByteString(crypto.pubKeyFromKeyPair(kp)))
223+
import Ordering.Implicits._
224+
val actualSigners = invalidCheckpoint.signatures.flatMap(_.publicKey(validBlockParent.hash)).sortBy(_.toSeq)
225+
val duplicatedSigner = ByteString(crypto.pubKeyFromKeyPair(keys.head))
226+
val expectedSigners =
227+
(keys.map(kp => ByteString(crypto.pubKeyFromKeyPair(kp))) :+ duplicatedSigner)
228+
.sortBy(_.toSeq)
211229
actualSigners shouldEqual expectedSigners
212230

213231
val headerWithInvalidCheckpoint = checkpointBlockGenerator
@@ -219,15 +237,17 @@ class BlockWithCheckpointHeaderValidatorSpec
219237

220238
val expectedError = {
221239
val invalidSigs =
222-
invalidCheckpoint.signatures.take(2).map(_ -> Some(ByteStringUtils.hash2string(expectedSigners.head)))
240+
invalidCheckpoint.signatures
241+
.filter(_.publicKey(validBlockParent.hash).contains(duplicatedSigner))
242+
.map(_ -> Some(ByteStringUtils.hash2string(duplicatedSigner)))
223243
Left(HeaderInvalidCheckpointSignatures(invalidSigs))
224244
}
225245

226246
blockHeaderValidator.validate(headerWithInvalidCheckpoint, validBlockParentHeader) shouldBe expectedError
227247
}
228248

229249
it should "return when failure when checkpoint has too many signatures" in new TestSetup {
230-
val invalidCheckpoint = validCheckpoint.copy(signatures = validCheckpoint.signatures ++ validCheckpoint.signatures)
250+
val invalidCheckpoint = validCheckpoint.copy(signatures = (validCheckpoint.signatures ++ validCheckpoint.signatures).sorted)
231251
val invalidBlockHeaderExtraFields = HefPostEcip1097(false, Some(invalidCheckpoint))
232252
val invalidBlockHeader = validBlockHeaderWithCheckpoint.copy(extraFields = invalidBlockHeaderExtraFields)
233253

@@ -255,7 +275,6 @@ class BlockWithCheckpointHeaderValidatorSpec
255275
"6848a3ab71918f57d3b9116b8e93c6fbc53e8a28dcd63e99c514dceee30fdd9741050fa7646bd196c9512e52f0d03097678c707996fff55587cd467801a1eee1"
256276
)
257277
)
258-
259278
val config: BlockchainConfig = blockchainConfig.copy(
260279
ecip1097BlockNumber = validBlockParentHeader.number,
261280
ecip1098BlockNumber = validBlockParentHeader.number,

0 commit comments

Comments
 (0)