add Ethereum block validation for HEAD subscription
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumBlockValidator
|
||||
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.Disposable
|
||||
@@ -25,7 +26,8 @@ import reactor.core.scheduler.Schedulers
|
||||
import reactor.kotlin.core.publisher.toMono
|
||||
|
||||
abstract class AbstractHead(
|
||||
private val forkChoice: ForkChoice
|
||||
private val forkChoice: ForkChoice,
|
||||
private val blockValidator: BlockValidator = BlockValidator.ALWAYS_VALID
|
||||
) : Head {
|
||||
|
||||
companion object {
|
||||
@@ -56,17 +58,22 @@ abstract class AbstractHead(
|
||||
}
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.subscribe { block ->
|
||||
notifyBeforeBlock()
|
||||
when (val choiceResult = forkChoice.choose(block)) {
|
||||
is ForkChoice.ChoiceResult.Updated -> {
|
||||
val newHead = choiceResult.nwhead
|
||||
log.debug("New block ${newHead.height} ${newHead.hash}")
|
||||
val result = stream.tryEmitNext(newHead)
|
||||
if (result.isFailure && result != Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER) {
|
||||
log.warn("Failed to dispatch block: $result as ${this.javaClass}")
|
||||
if (blockValidator.isValid(block)) {
|
||||
notifyBeforeBlock()
|
||||
when (val choiceResult = forkChoice.choose(block)) {
|
||||
is ForkChoice.ChoiceResult.Updated -> {
|
||||
val newHead = choiceResult.nwhead
|
||||
log.debug("New block ${newHead.height} ${newHead.hash}")
|
||||
val result = stream.tryEmitNext(newHead)
|
||||
if (result.isFailure && result != Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER) {
|
||||
log.warn("Failed to dispatch block: $result as ${this.javaClass}")
|
||||
}
|
||||
}
|
||||
|
||||
is ForkChoice.ChoiceResult.Same -> {}
|
||||
}
|
||||
is ForkChoice.ChoiceResult.Same -> {}
|
||||
} else {
|
||||
log.warn("Invalid block $block}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
|
||||
interface BlockValidator {
|
||||
|
||||
fun isValid(block: BlockContainer): Boolean
|
||||
|
||||
class AlwaysValid : BlockValidator {
|
||||
override fun isValid(block: BlockContainer): Boolean = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ALWAYS_VALID = AlwaysValid()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.AbstractHead
|
||||
import io.emeraldpay.dshackle.upstream.BlockValidator
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
@@ -29,7 +30,7 @@ import reactor.core.publisher.Mono
|
||||
|
||||
open class DefaultEthereumHead(
|
||||
forkChoice: ForkChoice
|
||||
) : Head, AbstractHead(forkChoice) {
|
||||
) : Head, AbstractHead(forkChoice, EthereumBlockValidator()) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(DefaultEthereumHead::class.java)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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.dshackle.upstream.BlockValidator
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.RLP.Companion.encode
|
||||
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 org.bouncycastle.jcajce.provider.digest.Keccak
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.math.BigInteger
|
||||
|
||||
class EthereumBlockValidator : BlockValidator {
|
||||
|
||||
companion object {
|
||||
val MAX_BIG_INT: BigInteger = BigInteger.valueOf(2).pow(256)
|
||||
val NUMBER_ELEMENTS = setOf("difficulty", "number", "gasLimit", "gasUsed", "timestamp", "baseFeePerGas")
|
||||
val ELEMENTS = setOf(
|
||||
"parentHash",
|
||||
"sha3Uncles",
|
||||
"miner",
|
||||
"stateRoot",
|
||||
"transactionsRoot",
|
||||
"receiptsRoot",
|
||||
"logsBloom",
|
||||
"difficulty",
|
||||
"number",
|
||||
"gasLimit",
|
||||
"gasUsed",
|
||||
"timestamp",
|
||||
"extraData",
|
||||
"mixHash",
|
||||
"nonce",
|
||||
"baseFeePerGas"
|
||||
)
|
||||
|
||||
private val log = LoggerFactory.getLogger(EthereumBlockValidator::class.java)
|
||||
}
|
||||
|
||||
override fun isValid(block: BlockContainer): Boolean =
|
||||
block.json?.let {
|
||||
println(String(it))
|
||||
val node = Global.objectMapper.readTree(it)
|
||||
val rlpEncoded = rlp(node)
|
||||
val hashValid = validateHash(block.hash, rlpEncoded)
|
||||
|
||||
if (!hashValid) {
|
||||
log.warn("Hash not valid for block ${block.hash}")
|
||||
}
|
||||
val difficultyValid = validateDifficulty(node, rlpEncoded)
|
||||
if (!difficultyValid) {
|
||||
log.warn("PoW not valid for block ${block.hash}")
|
||||
}
|
||||
|
||||
hashValid && difficultyValid
|
||||
} ?: false
|
||||
|
||||
private fun rlp(node: JsonNode): Map<String, ByteArray> =
|
||||
ELEMENTS.mapNotNull {
|
||||
node.get(it)?.asText()?.let { text ->
|
||||
it to text
|
||||
}
|
||||
}.associate {
|
||||
it.first to if (NUMBER_ELEMENTS.contains(it.first)) encodeBigInt(fromHexStringI(it.second)) else encode(
|
||||
fromHexString(it.second)
|
||||
)
|
||||
}
|
||||
|
||||
private fun validateHash(blockHash: BlockId, rlpEncoded: Map<String, ByteArray>): Boolean {
|
||||
val elements = ELEMENTS.mapNotNull { rlpEncoded[it] }.toList()
|
||||
val encoded = encodeList(elements)
|
||||
return blockHash.value.contentEquals(sha3(encoded))
|
||||
}
|
||||
|
||||
private fun validateDifficulty(node: JsonNode, rlpEncoded: Map<String, ByteArray>): Boolean {
|
||||
val nonce = fromHexStringI(node["nonce"].asText())
|
||||
val difficulty = fromHexStringI(node["difficulty"].asText())
|
||||
val mix = fromHexString(node["mixHash"].asText())
|
||||
|
||||
val target = MAX_BIG_INT.divide(difficulty)
|
||||
val encoded = encodeList(
|
||||
ELEMENTS.filterNot { it in listOf("nonce", "mixHash") }
|
||||
.mapNotNull { rlpEncoded[it] }
|
||||
.toList()
|
||||
)
|
||||
val sha3 = sha3(encoded)
|
||||
val seed = Keccak.Digest512().digest(sha3.plus(nonce.asUint64()))
|
||||
val result = sha3(seed.plus(mix))
|
||||
return target >= BigInteger(1, result)
|
||||
}
|
||||
|
||||
private fun BigInteger.asUint64() = this.toByteArray().let { bytes ->
|
||||
ByteArray(8) { bytes[bytes.size - it - 1] }
|
||||
}
|
||||
|
||||
private fun sha3(byteArray: ByteArray): ByteArray =
|
||||
Keccak.Digest256().digest(byteArray)
|
||||
}
|
||||
102
src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/RLP.kt
Normal file
102
src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/RLP.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package io.emeraldpay.dshackle.upstream.ethereum
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
class RLP {
|
||||
companion object {
|
||||
private const val SIZE_THRESHOLD = 56
|
||||
private const val OFFSET_SHORT_ITEM = 0x80
|
||||
private const val OFFSET_LONG_ITEM = 0xb7
|
||||
private const val OFFSET_SHORT_LIST = 0xc0
|
||||
private const val OFFSET_LONG_LIST = 0xf7
|
||||
|
||||
@JvmStatic
|
||||
fun toHexString(bytes: ByteArray) = bytes.toHexString()
|
||||
|
||||
@JvmStatic
|
||||
fun fromHexString(hexString: String?): ByteArray =
|
||||
hexString?.let { hex ->
|
||||
hex.replace("0x", "")
|
||||
.takeIf { it.length % 2 == 0 }
|
||||
?.chunked(2)
|
||||
?.map { it.toInt(16).toByte() }
|
||||
?.toByteArray()
|
||||
?: throw IllegalArgumentException("Invalid HEX string '$hexString'")
|
||||
} ?: byteArrayOf()
|
||||
|
||||
@JvmStatic
|
||||
fun fromHexStringI(hexString: String?): BigInteger =
|
||||
hexString?.let { hex ->
|
||||
val replace = hex.replace("0x", "")
|
||||
BigInteger(replace, 16)
|
||||
} ?: BigInteger.ZERO
|
||||
|
||||
@JvmStatic
|
||||
fun encodeBigInt(value: BigInteger) =
|
||||
encode(value.asUnsignedByteArray())
|
||||
|
||||
@JvmStatic
|
||||
fun encodeString(str: String) =
|
||||
encode(str.toByteArray())
|
||||
|
||||
@JvmStatic
|
||||
fun encode(srcData: ByteArray?): ByteArray {
|
||||
if (srcData == null || srcData.isEmpty()) {
|
||||
return byteArrayOf(OFFSET_SHORT_ITEM.toByte())
|
||||
}
|
||||
if (srcData.size == 1 && (srcData[0].toInt() and 0xFF < OFFSET_SHORT_ITEM)) {
|
||||
return srcData
|
||||
}
|
||||
if (srcData.size < SIZE_THRESHOLD) {
|
||||
val length = OFFSET_SHORT_ITEM + srcData.size
|
||||
return byteArrayOf(length.toByte()).plus(srcData)
|
||||
}
|
||||
val len = toMinimalByteArray(srcData.size)
|
||||
return byteArrayOf((OFFSET_LONG_ITEM + len.size).toByte()).plus(len).plus(srcData)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun encodeStringList(vararg elements: String) =
|
||||
encodeList(elements.map { it.toByteArray() }.map { encode(it) })
|
||||
|
||||
@JvmStatic
|
||||
fun encodeList(elements: List<ByteArray>): ByteArray {
|
||||
val totalLength = elements.sumOf { it.size }
|
||||
if (totalLength < SIZE_THRESHOLD) {
|
||||
return byteArrayOf((OFFSET_SHORT_LIST + totalLength).toByte()).plus(
|
||||
elements.reduce { a, b -> a.plus(b) }
|
||||
)
|
||||
}
|
||||
|
||||
val len = toMinimalByteArray(totalLength)
|
||||
return byteArrayOf((OFFSET_LONG_LIST + len.size).toByte())
|
||||
.plus(len)
|
||||
.plus(elements.reduce { a, b -> a.plus(b) })
|
||||
}
|
||||
|
||||
private fun toMinimalByteArray(value: Int): ByteArray {
|
||||
val encoded = toByteArray(value)
|
||||
for (i in encoded.indices) {
|
||||
if (encoded[i].toInt() != 0) {
|
||||
return encoded.copyOfRange(i, encoded.size)
|
||||
}
|
||||
}
|
||||
return byteArrayOf()
|
||||
}
|
||||
|
||||
private fun toByteArray(value: Int): ByteArray {
|
||||
return byteArrayOf(
|
||||
(value shr 24 and 0xff).toByte(),
|
||||
(value shr 16 and 0xff).toByte(),
|
||||
(value shr 8 and 0xff).toByte(),
|
||||
(value and 0xff).toByte()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString() = joinToString(separator = "") { "%02x".format(it) }.uppercase()
|
||||
fun BigInteger.asUnsignedByteArray(): ByteArray =
|
||||
toByteArray().let {
|
||||
if (it[0] == 0.toByte()) it.copyOfRange(1, it.size) else it
|
||||
}
|
||||
Reference in New Issue
Block a user