Merge pull request #4 from p2p-org/ethereum_block_validation

add Ethereum block validation for HEAD subscription
This commit is contained in:
MaxFomenkov
2022-08-26 13:12:04 +03:00
committed by GitHub
12 changed files with 530 additions and 12 deletions

View File

@@ -25,7 +25,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 +57,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(forkChoice.getHead(), 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(currentHead: BlockContainer?, newHead: BlockContainer): Boolean
class AlwaysValid : BlockValidator {
override fun isValid(currentHead: BlockContainer?, newHead: BlockContainer): Boolean = true
}
companion object {
val ALWAYS_VALID = AlwaysValid()
}
}

View File

@@ -29,7 +29,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,183 @@
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 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(
"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(currentHead: BlockContainer?, newHead: BlockContainer): Boolean =
newHead.json?.let {
val validBlocksSequence = currentHead?.let { cur -> validateTotalDifficulty(cur, newHead) } ?: true
val node = Global.objectMapper.readTree(it)
val rlpEncoded = rlp(node)
val hashValid = validateHash(newHead.hash, rlpEncoded)
if (!hashValid) {
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 ${newHead.hash}")
}
hashValid && difficultyValid && validBlocksSequence
} ?: 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 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)
val encoded = encodeList(
ELEMENTS.filterNot { it in listOf("nonce", "mixHash") }
.mapNotNull { rlpEncoded[it] }
.toList()
)
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) {
val index = bytes.size - it - 1
if (index < bytes.size) bytes[index] else 0
}
}
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
}

View File

@@ -132,7 +132,7 @@ class AbstractHeadSpec extends Specification {
BlockContainer getHead() {
return null
}
})
}, new BlockValidator.AlwaysValid())
}
}
}

View File

@@ -0,0 +1,57 @@
package io.emeraldpay.dshackle.upstream.ethereum
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
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"
validator.isValid(null, fromFile(path)) == expect
where:
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,67 @@
package io.emeraldpay.dshackle.upstream.ethereum
import spock.lang.Specification
import static io.emeraldpay.dshackle.upstream.ethereum.RLP.toHexString
class RLPSpec extends Specification {
def "encode bytes"() {
expect:
toHexString(RLP.encode(input)) == output
where:
input || output
new byte[0] || "80"
new byte[]{0} || "00"
new byte[]{1} || "01"
new byte[]{0x7F} || "7F"
new byte[]{0x80} || "8180"
new byte[]{0xFF} || "81FF"
new byte[]{1, 2, 3} || "83010203"
longByteArray() || "B8390102030000000000000000000000000000000000000000000000000000000000000000000000000" +
"00000000000000000000000000000000000"
}
def "encode string"() {
expect:
toHexString(RLP.encodeString(input)) == output
where:
input || output
"dog" || "83646F67"
"Lorem ipsum dolor sit amet, consectetur adipisicing eli" || "B74C6F72656D20697073756D20646F6C6F72207369742" +
"0616D65742C20636F6E7365637465747572206164697069736963696E6720656C69"
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat" || "B904004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E73656374657475722061646970697363696E6720656C69742E20437572616269747572206D6175726973206D61676E612C20737573636970697420736564207665686963756C61206E6F6E2C20696163756C697320666175636962757320746F72746F722E2050726F696E20737573636970697420756C74726963696573206D616C6573756164612E204475697320746F72746F7220656C69742C2064696374756D2071756973207472697374697175652065752C20756C7472696365732061742072697375732E204D6F72626920612065737420696D70657264696574206D6920756C6C616D636F7270657220616C6971756574207375736369706974206E6563206C6F72656D2E2041656E65616E2071756973206C656F206D6F6C6C69732C2076756C70757461746520656C6974207661726975732C20636F6E73657175617420656E696D2E204E756C6C6120756C74726963657320747572706973206A7573746F2C20657420706F73756572652075726E6120636F6E7365637465747572206E65632E2050726F696E206E6F6E20636F6E76616C6C6973206D657475732E20446F6E65632074656D706F7220697073756D20696E206D617572697320636F6E67756520736F6C6C696369747564696E2E20566573746962756C756D20616E746520697073756D207072696D697320696E206661756369627573206F726369206C756374757320657420756C74726963657320706F737565726520637562696C69612043757261653B2053757370656E646973736520636F6E76616C6C69732073656D2076656C206D617373612066617563696275732C2065676574206C6163696E6961206C616375732074656D706F722E204E756C6C61207175697320756C747269636965732070757275732E2050726F696E20617563746F722072686F6E637573206E69626820636F6E64696D656E74756D206D6F6C6C69732E20416C697175616D20636F6E73657175617420656E696D206174206D65747573206C75637475732C206120656C656966656E6420707572757320656765737461732E20437572616269747572206174206E696268206D657475732E204E616D20626962656E64756D2C206E6571756520617420617563746F72207472697374697175652C206C6F72656D206C696265726F20616C697175657420617263752C206E6F6E20696E74657264756D2074656C6C7573206C65637475732073697420616D65742065726F732E20437261732072686F6E6375732C206D65747573206163206F726E617265206375727375732C20646F6C6F72206A7573746F20756C747269636573206D657475732C20617420756C6C616D636F7270657220766F6C7574706174"
}
def "encode bigint"() {
expect:
toHexString(RLP.encodeBigInt(BigInteger.valueOf(input))) == output
where:
input || output
0 || "80"
1 || "01"
127 || "7F"
128 || "8180"
256 || "820100"
1024 || "820400"
0xFFFFFF || "83FFFFFF"
0xFFFFFFFFFFFF || "86FFFFFFFFFFFF"
}
def "encode string list"() {
expect:
toHexString(RLP.encodeStringList("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo")) == "F83C836161618362626283636363836464648365656583666666836767678368686883696969836A6A6A836B6B6B836C6C6C836D6D6D836E6E6E836F6F6F"
}
def longByteArray() {
def manyBytes = new byte[57]
manyBytes[0] = 1
manyBytes[1] = 2
manyBytes[2] = 3
return manyBytes
}
}

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

@@ -0,0 +1,18 @@
{
"hash": "0xae174c5cf816f820b17ae13d120fff057406ce12f448422f0d522459d4ae646b",
"parentHash": "0xd783efa4d392943503f28438ad5830b2d5964696ffc285f338585e9fe0a37a05",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"miner": "0xc0ea08a2d404d3172d2add29a45be56da40e2949",
"stateRoot": "0x77d14e10470b5850332524f8cd6f69ad21f070ce92dca33ab2858300242ef2f1",
"transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"difficulty": "0x98BA212B716C",
"number": "0x3285D2",
"gasLimit": "0x3D4642",
"gasUsed": "0x0",
"timestamp": "0x58BF4098",
"mixHash": "0x3e140b0784516af5e5ec6730f2fb20cca22f32be399b9e4ad77d32541f798cd0",
"nonce": "0xf400cd0006070c49",
"extraData": "0x7777772E62772E636F6D",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}