add Ethereum block validation for HEAD subscription

This commit is contained in:
Maxksim Fomenkov
2022-08-23 18:59:39 +03:00
parent cef33d5092
commit 6c96d54cfa
10 changed files with 376 additions and 12 deletions

View File

@@ -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}")
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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)
}

View 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
}