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 =