added total difficulty & blocks sequence checks

This commit is contained in:
Maxksim Fomenkov
2022-08-25 13:40:40 +03:00
parent f3e75d9680
commit 7eea1cc6f1
8 changed files with 203 additions and 46 deletions

View File

@@ -57,7 +57,7 @@ abstract class AbstractHead(
}
.subscribeOn(Schedulers.boundedElastic())
.subscribe { block ->
if (blockValidator.isValid(block)) {
if (blockValidator.isValid(forkChoice.getHead(), block)) {
notifyBeforeBlock()
when (val choiceResult = forkChoice.choose(block)) {
is ForkChoice.ChoiceResult.Updated -> {

View File

@@ -4,10 +4,10 @@ import io.emeraldpay.dshackle.data.BlockContainer
interface BlockValidator {
fun isValid(block: BlockContainer): Boolean
fun isValid(currentHead: BlockContainer?, newHead: BlockContainer): Boolean
class AlwaysValid : BlockValidator {
override fun isValid(block: BlockContainer): Boolean = true
override fun isValid(currentHead: BlockContainer?, newHead: BlockContainer): Boolean = true
}
companion object {

View File

@@ -10,13 +10,16 @@ import io.emeraldpay.dshackle.upstream.ethereum.RLP.Companion.encodeBigInt
import io.emeraldpay.dshackle.upstream.ethereum.RLP.Companion.encodeList
import io.emeraldpay.dshackle.upstream.ethereum.RLP.Companion.fromHexString
import io.emeraldpay.dshackle.upstream.ethereum.RLP.Companion.fromHexStringI
import io.emeraldpay.etherjar.rpc.json.BlockJson
import org.bouncycastle.jcajce.provider.digest.Keccak
import org.slf4j.LoggerFactory
import java.math.BigDecimal
import java.math.BigInteger
class EthereumBlockValidator : BlockValidator {
companion object {
val ACCURACY = 0.02.toBigDecimal()
val MAX_BIG_INT: BigInteger = BigInteger.valueOf(2).pow(256)
val NUMBER_ELEMENTS = setOf("difficulty", "number", "gasLimit", "gasUsed", "timestamp", "baseFeePerGas")
val ELEMENTS = setOf(
@@ -41,22 +44,22 @@ class EthereumBlockValidator : BlockValidator {
private val log = LoggerFactory.getLogger(EthereumBlockValidator::class.java)
}
override fun isValid(block: BlockContainer): Boolean =
block.json?.let {
override fun isValid(currentHead: BlockContainer?, newHead: BlockContainer): Boolean =
newHead.json?.let {
println(String(it))
val validBlocksSequence = currentHead?.let { cur -> validateTotalDifficulty(cur, newHead) } ?: true
val node = Global.objectMapper.readTree(it)
val rlpEncoded = rlp(node)
val hashValid = validateHash(block.hash, rlpEncoded)
val hashValid = validateHash(newHead.hash, rlpEncoded)
if (!hashValid) {
log.warn("Hash not valid for block ${block.hash}")
log.warn("Hash '${newHead.hash}' not valid for block ${newHead.hash}")
}
val difficultyValid = validateDifficulty(node, rlpEncoded)
if (!difficultyValid) {
log.warn("PoW not valid for block ${block.hash}")
log.warn("PoW not valid for block ${newHead.hash}")
}
hashValid && difficultyValid
hashValid && difficultyValid && validBlocksSequence
} ?: false
private fun rlp(node: JsonNode): Map<String, ByteArray> =
@@ -77,8 +80,11 @@ class EthereumBlockValidator : BlockValidator {
}
private fun validateDifficulty(node: JsonNode, rlpEncoded: Map<String, ByteArray>): Boolean {
val nonce = fromHexStringI(node["nonce"].asText())
val difficulty = fromHexStringI(node["difficulty"].asText())
if (difficulty.signum() < 0) {
return false
}
val nonce = fromHexStringI(node["nonce"].asText())
val mix = fromHexString(node["mixHash"].asText())
val target = MAX_BIG_INT.divide(difficulty)
@@ -87,14 +93,90 @@ class EthereumBlockValidator : BlockValidator {
.mapNotNull { rlpEncoded[it] }
.toList()
)
val sha3 = sha3(encoded)
val seed = Keccak.Digest512().digest(sha3.plus(nonce.asUint64()))
val headerHash = sha3(encoded)
val seed = Keccak.Digest512().digest(headerHash.plus(nonce.asUint64()))
val result = sha3(seed.plus(mix))
return target >= BigInteger(1, result)
}
private fun validateTotalDifficulty(currentHead: BlockContainer, newHead: BlockContainer): Boolean =
currentHead.getParsed(BlockJson::class.java)?.let { cur ->
newHead.getParsed(BlockJson::class.java)?.let {
val numberValid = it.number >= cur.number
if (!numberValid) {
log.warn(
"Block number {} not valid for {}. Must be greater than {}",
it.number,
it.hash,
cur.number
)
}
val timestampValid = it.timestamp > cur.timestamp
if (!timestampValid) {
log.warn(
"Block timestamp {} not valid for {}. Must be greater than {}",
it.timestamp,
it.hash,
cur.timestamp
)
}
numberValid && timestampValid && approxTotalDifficultyValid(
curTotalDifficulty = cur.totalDifficulty,
curDifficulty = cur.difficulty,
curNumber = cur.number,
blockTotalDifficulty = it.totalDifficulty,
blockNumber = it.number,
blockHash = it.hash.toHex()
)
} ?: false
} ?: true
private fun approxTotalDifficultyValid(
curTotalDifficulty: BigInteger,
curDifficulty: BigInteger,
curNumber: Long,
blockTotalDifficulty: BigInteger,
blockNumber: Long,
blockHash: String
): Boolean {
if (blockTotalDifficulty < curTotalDifficulty) {
log.warn(
"Block totalDifficulty {} not valid for {}. Must be greater than {}",
blockTotalDifficulty,
blockHash,
curTotalDifficulty
)
return false
}
val decimalRef = curDifficulty.toBigDecimal()
val shift = BigDecimal.valueOf(blockNumber - curNumber)
val totalAccuracy = shift * ACCURACY
val blockApprox = (blockTotalDifficulty - curTotalDifficulty).toBigDecimal() / shift
val min = decimalRef * (BigDecimal.ONE - totalAccuracy)
val max = decimalRef * (BigDecimal.ONE + totalAccuracy)
val valid = blockApprox in min..max
if (!valid) {
log.warn(
"Block totalDifficulty {} not valid for {}. Expected to be int [{}, {}]",
blockTotalDifficulty,
blockHash,
min.toBigInteger(),
max.toBigInteger()
)
}
return valid
}
private fun BigInteger.asUint64() = this.toByteArray().let { bytes ->
ByteArray(8) { bytes[bytes.size - it - 1] }
ByteArray(8) {
val index = bytes.size - it - 1
if (index < bytes.size) bytes[index] else 0
}
}
private fun sha3(byteArray: ByteArray): ByteArray =

View File

@@ -1,29 +1,57 @@
package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.JsonNode
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.etherjar.rpc.json.BlockJson
import org.apache.commons.io.FileUtils
import spock.lang.Specification
import java.time.Instant
class EthereumBlockValidatorSpec extends Specification {
def mapper = Global.getObjectMapper()
def validator = new EthereumBlockValidator()
def "test validate block"() {
expect:
def path = "src/test/resources/blocks/${file}.json"
def bytes = FileUtils.readFileToByteArray(new File(path))
def container = new BlockContainer(1L, BlockId.from(hash), BigInteger.ONE, Instant.now(), false, bytes, null, Collections.emptyList(), 1)
validator.isValid(container) == expect
validator.isValid(null, fromFile(path)) == expect
where:
file || expect || hash
"eth_valid_block_1" || true || "0xae174c5cf816f820b17ae13d120fff057406ce12f448422f0d522459d4ae646b"
"eth_valid_block_2" || true || "0x95fd7b85ed04cc67884d7c33d13df3ebb568a2c1532021316c296038bda71776"
file || expect
"eth_valid_block_1" || true
"eth_block_sec_1" || true
"eth_block_sec_2" || true
"eth_block_sec_2_invalid" || true
}
def "test validate blocks sequence"() {
expect:
def curPath = "src/test/resources/blocks/${cur}.json"
def nextPath = "src/test/resources/blocks/${next}.json"
validator.isValid(fromFile(curPath), fromFile(nextPath)) == expect
where:
cur || next || expect
"eth_block_sec_1" || "eth_block_sec_2" || true
"eth_block_sec_2" || "eth_block_sec_1" || false
"eth_block_sec_1" || "eth_block_sec_2_invalid" || false
}
def fromFile(String path) {
def bytes = FileUtils.readFileToByteArray(new File(path))
def block = mapper.readValue(bytes, BlockJson.class)
return new BlockContainer(
block.number,
BlockId.from(block.hash.toHex()),
block.difficulty,
block.timestamp,
true, bytes,
block,
Collections.emptyList(),
1)
}
}

View File

@@ -0,0 +1,23 @@
{
"baseFeePerGas": "0x121e9f5ec",
"difficulty": "0x2a76b9975fb402",
"extraData": "0x457468657265756d50504c4e532f326d696e6572735f455536",
"gasLimit": "0x1c9c380",
"gasUsed": "0x1c995f7",
"hash": "0xb359a505ab41388cb97f3d7a270f9a5a70ee46f3b496c5f5f367f3b13c6858bf",
"logsBloom": "0x37fbc56e6b8fd9bef7f7ffd0ff7557f386b67de3cd2c433a7a7fdafa3db9c96f7de997ecde4c7f75540e9bf7607f1baebe79bffaae7ffb9ff783dce2b5f7e3e3f75fc8a75bf8a5fff9cf63fb5be179e10ad75eec9f7e9b5b31fbfed7c85749bfff6dc82532ff7fa999dfdddcf7e46efb3a5f2c7f8e5fa7777f23dff6f5ffe60afb359fee6e639e7fd94dafe1eff7a8676f78efc39f7cf3ddadafbffb4eb6b4dbffd263f9ffbfff708ef7ffbbfd56edbdcfefbed6aaae0f9b7b6fdff7feffe67dc7afbcfeedb2ad73ffefbd2edc9f5ceb1f6c37f1b17feeb77b5fdf3f727f69fbb476ffcaf8773de7e7ff3725097c4a8db577de793ede8df472ccde9d7e0aff7e",
"miner": "0x00192fb10df37c9fb26829eb2cc623cd1bf599e8",
"mixHash": "0x61b14f607b1c9ff2aa14e65ce8e87f2831cd37e1a0b46cfeac8e6fb8d35b9cb0",
"nonce": "0x1da600070ccbad45",
"number": "0xeb1db1",
"parentHash": "0x17b1fe61ce7690725af12865b0f63e20c9c5944748cfbba6041362acc8ca25b4",
"receiptsRoot": "0xcec742556a657826cd4d9ceaf00c2a241a28d6145b6e4b6af077820c710930c6",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"size": "0x2397d",
"stateRoot": "0x2a2773fd8b4d55e2e777bb71adc4d99d913d9520be2026e26dd6c8c22527a204",
"timestamp": "0x63074d3c",
"totalDifficulty": "0xc1b02a29da16ecdfb95",
"transactions": [],
"transactionsRoot": "0xb74a34aeee1e746f60747225b9671d71df22dd368a308ef436059a0e7877c896",
"uncles": []
}

View File

@@ -0,0 +1,23 @@
{
"baseFeePerGas": "0x1461ffefd",
"difficulty": "0x2a6c5be8f9dc16",
"extraData": "0x4554482e4352415a59504f4f4c2e4f5247",
"gasLimit": "0x1ca35ef",
"gasUsed": "0x1ca11b3",
"hash": "0xc3c93de6e76e5da05df8a48ab15dfb0cc610d21932a0220e797e0adc05118ed8",
"logsBloom": "0xffb4778ec39a1aeef1b072f2bb1f47bb91266fd117530aeb7fbdda78deee7d6ff39e5ff8b7d83ad061bf1b9e083709ff0abfbe6afbfffdd73e49f77f80ff377d87dfa1de3f7d94bff9f66bbf5060f9ed20d74468ffff9c29ff07df4de9fdbffa1eed92e043fbc7dbf95efff9aef7ace1bace1e7a94de65e7527beed2454deeceeb609fbc9256d1fbbf6dbee5975335775f72fcd5e7e2585bad718cd278bbf66cefdbefe17acb76375befcbf9fd9ceb9d57d6fbe6ef7ae03fcb7b3eeed9fde946ffb9f5be4d8becf76f6c2633c73ed7c20df1f6e29c7e5bff1f9c79365e3d6dd5bc7afbbbcd7e1afbe5b657f8ab66ac768ee6ace959f9a77f78f9bf95babbffa7",
"miner": "0x4f9bebe3adc3c7f647c0023c60f91ac9dffa52d5",
"mixHash": "0xf6d6b09cae8efa94f2caa018c38c5c17ab1d89ab3d84feca076d1894f2f07a8f",
"nonce": "0xf7d487f95819f012",
"number": "0xeb1db2",
"parentHash": "0xb359a505ab41388cb97f3d7a270f9a5a70ee46f3b496c5f5f367f3b13c6858bf",
"receiptsRoot": "0x4e3cd00367e2a59339612ed2d10dcec5ec9ccdcbeb66a343bf1a8a115b9ce04e",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"size": "0x2086f",
"stateRoot": "0x8e2f50554d5e8bb783f12db999b5ff8a948e980a80593e877574eaa089ad362b",
"timestamp": "0x63074d58",
"totalDifficulty": "0xc1b02cd09fd57c7d7ab",
"transactions": [],
"transactionsRoot": "0x22b3c3ce7398ea4ee2665a5f13a7b77c4828802f160fc261f82b551f0a37a97c",
"uncles": []
}

View File

@@ -0,0 +1,23 @@
{
"baseFeePerGas": "0x1461ffefd",
"difficulty": "0x2a6c5be8f9dc16",
"extraData": "0x4554482e4352415a59504f4f4c2e4f5247",
"gasLimit": "0x1ca35ef",
"gasUsed": "0x1ca11b3",
"hash": "0xc3c93de6e76e5da05df8a48ab15dfb0cc610d21932a0220e797e0adc05118ed8",
"logsBloom": "0xffb4778ec39a1aeef1b072f2bb1f47bb91266fd117530aeb7fbdda78deee7d6ff39e5ff8b7d83ad061bf1b9e083709ff0abfbe6afbfffdd73e49f77f80ff377d87dfa1de3f7d94bff9f66bbf5060f9ed20d74468ffff9c29ff07df4de9fdbffa1eed92e043fbc7dbf95efff9aef7ace1bace1e7a94de65e7527beed2454deeceeb609fbc9256d1fbbf6dbee5975335775f72fcd5e7e2585bad718cd278bbf66cefdbefe17acb76375befcbf9fd9ceb9d57d6fbe6ef7ae03fcb7b3eeed9fde946ffb9f5be4d8becf76f6c2633c73ed7c20df1f6e29c7e5bff1f9c79365e3d6dd5bc7afbbbcd7e1afbe5b657f8ab66ac768ee6ace959f9a77f78f9bf95babbffa7",
"miner": "0x4f9bebe3adc3c7f647c0023c60f91ac9dffa52d5",
"mixHash": "0xf6d6b09cae8efa94f2caa018c38c5c17ab1d89ab3d84feca076d1894f2f07a8f",
"nonce": "0xf7d487f95819f012",
"number": "0xeb1db2",
"parentHash": "0xb359a505ab41388cb97f3d7a270f9a5a70ee46f3b496c5f5f367f3b13c6858bf",
"receiptsRoot": "0x4e3cd00367e2a59339612ed2d10dcec5ec9ccdcbeb66a343bf1a8a115b9ce04e",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"size": "0x2086f",
"stateRoot": "0x8e2f50554d5e8bb783f12db999b5ff8a948e980a80593e877574eaa089ad362b",
"timestamp": "0x63074d58",
"totalDifficulty": "0xc1b03cd09fd57c7d7ab",
"transactions": [],
"transactionsRoot": "0x22b3c3ce7398ea4ee2665a5f13a7b77c4828802f160fc261f82b551f0a37a97c",
"uncles": []
}

View File

@@ -1,22 +0,0 @@
{
"baseFeePerGas": "0x1bee2dd60",
"difficulty": "0x2a392ce7577bb6",
"extraData": "0x706f6f6c696e2e636f6d203bb704c2ea57e9ea",
"gasLimit": "0x1c95111",
"gasUsed": "0x1c92201",
"hash": "0x95fd7b85ed04cc67884d7c33d13df3ebb568a2c1532021316c296038bda71776",
"logsBloom": "0xcebecdfb7deb7ced9da83f9dfbbadf778315b7cb669d85bee9af5a76dcaebdbf9f7eb5ed9dd15d1ff4a1bb6f67ff4bdc4f1db5a95efbbbf4a655b2ff6d3ff7fde4fb98b99f59b5fefa921fd962296df2bcdbdfbf8ff55d7d12c2df1fbcbddadc5f12abc5e72af47ec084be69a17a3fdbff7dea6fff7d5ff60fdbecd2cf6cbebdcf789f4ce95fbbe416ef9bffcf6ea4f65fa9adf3fb6bdd1a7efffbfe49bbbbb5fbddab6df5ea7e689fa9ebf6ed76bdb497dc2bceff39f31a1e33fff7dcfd637cdb53f70fb3cf0cfb6ff0fbbebdfbd7dcaffaff46ea5f8b9e6e757ffefcfdf7dea17f7f7cffef7f4d5ea055fd7f6c7cafaeeb98b9496d0e5df7eedab1be5b79ef",
"miner": "0x8f03f1a3f10c05e7cccf75c1fd10168e06659be7",
"mixHash": "0x1071fcdfa97b5a7a7c65ff9376ad212815d89986c1530c8b23f84d31c13cad12",
"nonce": "0xe6de1dde14d49aeb",
"number": "0xeaf0bb",
"parentHash": "0x431533a3fec65477d91c310a78ac766c2c660b4c410db6c429cb71ca069cc441",
"receiptsRoot": "0x01253f659cdf7e7223ef4b254fa7e654c1813bb1f39676033ff34b18c65ccb83",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"size": "0x230b2",
"stateRoot": "0x482376d49e5a49695f28fbc43cadb7c09ec1486d0951280ca1d92590b5b83c8d",
"timestamp": "0x6304e043",
"totalDifficulty": "0xc1370904c375c59ceb9",
"transactionsRoot": "0x7ada19c3ef58d5470452dfa2a45611c632e36204b90a77da0419225f0364f713",
"uncles": []
}