added total difficulty & blocks sequence checks
This commit is contained in:
@@ -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 -> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user