Check chain settings via ws (#544)

This commit is contained in:
KirillPamPam
2024-08-12 18:27:47 +04:00
committed by GitHub
parent e8410d7638
commit 96487b6c5e
20 changed files with 449 additions and 59 deletions

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessState
import org.jetbrains.annotations.NotNull
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
@@ -89,7 +90,7 @@ class EthereumHeadMock implements Head {
}
@Override
Flux<Boolean> headLiveness() {
Flux<HeadLivenessState> headLiveness() {
return Flux.empty()
}

View File

@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessState
import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector
import io.emeraldpay.dshackle.upstream.ChainRequest
@@ -12,16 +13,16 @@ import reactor.core.publisher.Flux
class GenericConnectorMock implements GenericConnector {
Reader<ChainRequest, ChainResponse> api
Head head
Flux<Boolean> liveness
Flux<HeadLivenessState> liveness
GenericConnectorMock(Reader<ChainRequest, ChainResponse> api, Head head) {
this.api = api
this.head = head
this.liveness = Flux.just(false)
this.liveness = Flux.just(HeadLivenessState.NON_CONSECUTIVE)
}
@Override
Flux<Boolean> hasLiveSubscriptionHead() {
Flux<HeadLivenessState> headLivenessEvents() {
return liveness
}

View File

@@ -179,14 +179,14 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "get available subscriptions"() {
when:
def up1 = TestingCommons.upstream("test")
up1.getConnectorMock().setLiveness(Flux.just(false))
up1.getConnectorMock().setLiveness(Flux.just(HeadLivenessState.NON_CONSECUTIVE))
def ethereumSubscribe1 = new EthereumEgressSubscription(TestingCommons.multistream(up1) as GenericMultistream, Schedulers.boundedElastic(), null)
then:
ethereumSubscribe1.getAvailableTopics() == []
when:
def up2 = TestingCommons.upstream("test")
up2.getConnectorMock().setLiveness(Flux.just(true))
up2.getConnectorMock().setLiveness(Flux.just(HeadLivenessState.OK))
up2.stop()
up2.start()
def ethereumSubscribe2 = new EthereumEgressSubscription(TestingCommons.multistream(up2) as GenericMultistream, Schedulers.boundedElastic(), null)
@@ -194,7 +194,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
ethereumSubscribe2.getAvailableTopics().toSet() == [EthereumEgressSubscription.METHOD_LOGS, EthereumEgressSubscription.METHOD_NEW_HEADS].toSet()
when:
def up3 = TestingCommons.upstream("test")
up3.getConnectorMock().setLiveness(Flux.just(true))
up3.getConnectorMock().setLiveness(Flux.just(HeadLivenessState.OK))
up3.stop()
up3.start()
def ethereumSubscribe3 = new EthereumEgressSubscription(TestingCommons.multistream(up3) as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
@@ -202,7 +202,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
ethereumSubscribe3.getAvailableTopics().toSet() == [EthereumEgressSubscription.METHOD_LOGS, EthereumEgressSubscription.METHOD_NEW_HEADS, EthereumEgressSubscription.METHOD_PENDING_TXES].toSet()
when:
def up4 = TestingCommons.upstream(TestingCommons.api(), "eth_getBlockByNumber")
up4.getConnectorMock().setLiveness(Flux.just(true))
up4.getConnectorMock().setLiveness(Flux.just(HeadLivenessState.OK))
up4.stop()
up4.start()
def ethereumSubscribe4 = new EthereumEgressSubscription(TestingCommons.multistream(up4) as GenericMultistream, Schedulers.boundedElastic(), null)

View File

@@ -27,6 +27,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice
import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.ChainResponse
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsClient
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
@@ -69,6 +70,11 @@ class GenericWsHeadSpec extends Specification {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
1 * ws.subscribe(_) >> new WsSubscriptions.SubscribeData(
Flux.fromIterable([headBlock]), "id", new AtomicReference<String>("")
)
@@ -82,6 +88,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -113,6 +120,11 @@ class GenericWsHeadSpec extends Specification {
def apiMock = TestingCommons.api()
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
def connectionInfoSink = Sinks.many().multicast().directBestEffort()
def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
@@ -120,6 +132,8 @@ class GenericWsHeadSpec extends Specification {
new WsSubscriptions.SubscribeData(Flux.error(new RuntimeException()), "id", new AtomicReference<String>("")),
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id", new AtomicReference<String>(""))
]
1 * it.unsubscribe(new ChainRequest("eth_unsubscribe", new ListParams(""), 2, null, null, false)) >>
Mono.just(new ChainResponse("".bytes, null))
}
def head = new GenericWsHead(
@@ -131,6 +145,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -171,6 +186,11 @@ class GenericWsHeadSpec extends Specification {
Global.objectMapper.writeValueAsBytes(it)
}
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
def apiMock = TestingCommons.api()
def connectionInfoSink = Sinks.many().multicast().directBestEffort()
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], null)
@@ -195,6 +215,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -229,6 +250,11 @@ class GenericWsHeadSpec extends Specification {
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], null)
apiMock.answerOnce("eth_blockNumber", [], Mono.empty())
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
1 * subscribe(_) >>> [
@@ -245,6 +271,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -278,6 +305,11 @@ class GenericWsHeadSpec extends Specification {
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], null)
apiMock.answerOnce("eth_blockNumber", [], Mono.empty())
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
1 * subscribe(_) >>> [
@@ -294,6 +326,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -340,6 +373,11 @@ class GenericWsHeadSpec extends Specification {
apiMock.answerOnce("eth_getBlockByHash", ["0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8", false], null)
apiMock.answerOnce("eth_blockNumber", [], Mono.empty())
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
2 * subscribe(_) >>> [
@@ -357,6 +395,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -401,6 +440,12 @@ class GenericWsHeadSpec extends Specification {
def reader = Mock(Reader) {
1 * it.read(new ChainRequest("eth_getBlockByNumber", new ListParams("latest", false))) >> Mono.empty()
}
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
def subId = "subId"
def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> Flux.empty()
@@ -420,6 +465,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -450,6 +496,11 @@ class GenericWsHeadSpec extends Specification {
def apiMock = TestingCommons.api()
def pool = Mock(WsConnectionPool) {
getConnection() >> Mock(WsConnection)
}
def client = new JsonRpcWsClient(pool)
def connectionInfoSink = Sinks.many().multicast().directBestEffort()
def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
@@ -467,6 +518,7 @@ class GenericWsHeadSpec extends Specification {
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific.INSTANCE,
client,
Duration.ofSeconds(60),
)
@@ -479,7 +531,7 @@ class GenericWsHeadSpec extends Specification {
.then {
connectionInfoSink.tryEmitNext(new WsConnection.ConnectionInfo("id", WsConnection.ConnectionState.DISCONNECTED))
}
.expectNext(false)
.expectNext(HeadLivenessState.DISCONNECTED)
.thenCancel()
.verify(Duration.ofSeconds(1))
}

View File

@@ -21,19 +21,19 @@ class HeadLivenessValidatorSpec extends Specification{
head.nextBlock(TestingCommons.blockForEthereum(1))
head.nextBlock(TestingCommons.blockForEthereum(2))
head.nextBlock(TestingCommons.blockForEthereum(3))
}.expectNext(true).thenCancel().verify(Duration.ofSeconds(1))
}.expectNext(HeadLivenessState.OK).thenCancel().verify(Duration.ofSeconds(1))
}
def "emits false if head liveness emits false"() {
when:
def head = Mock(Head) {
1 * it.headLiveness() >> Flux.just(false)
1 * it.headLiveness() >> Flux.just(HeadLivenessState.NON_CONSECUTIVE)
1 * it.getFlux() >> Flux.just(TestingCommons.blockForEthereum(1))
}
def checker = new HeadLivenessValidatorImpl(head, Duration.ofSeconds(10), Schedulers.boundedElastic(), "test")
then:
StepVerifier.create(checker.flux)
.expectNext(false)
.expectNext(HeadLivenessState.NON_CONSECUTIVE)
.thenCancel()
.verify(Duration.ofSeconds(1))
}
@@ -52,7 +52,7 @@ class HeadLivenessValidatorSpec extends Specification{
.then {
head.nextBlock(TestingCommons.blockForEthereum(5))
}
.expectNext(false)
.expectNext(HeadLivenessState.NON_CONSECUTIVE)
.thenCancel().verify(Duration.ofSeconds(1))
}
@@ -67,7 +67,7 @@ class HeadLivenessValidatorSpec extends Specification{
head.nextBlock(TestingCommons.blockForEthereum(2))
}
.thenAwait(Duration.ofSeconds(1))
.expectNext(false)
.expectNext(HeadLivenessState.NON_CONSECUTIVE)
.thenCancel().verify(Duration.ofSeconds(2))
}
@@ -82,13 +82,13 @@ class HeadLivenessValidatorSpec extends Specification{
head.nextBlock(TestingCommons.blockForEthereum(2))
}
.thenAwait(Duration.ofSeconds(1))
.expectNext(false)
.expectNext(HeadLivenessState.NON_CONSECUTIVE)
.then {
head.nextBlock(TestingCommons.blockForEthereum(3))
head.nextBlock(TestingCommons.blockForEthereum(4))
head.nextBlock(TestingCommons.blockForEthereum(5))
}
.expectNext(true)
.expectNext(HeadLivenessState.OK)
.thenCancel().verify(Duration.ofSeconds(3))
}
}

View File

@@ -0,0 +1,248 @@
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.reader.ChainReader
import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.ChainResponse
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsClient
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks
import reactor.core.scheduler.Schedulers
import reactor.test.StepVerifier
import java.math.BigInteger
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.atomic.AtomicReference
class GenericWsHeadTest {
@Test
fun `validate chain settings and then head sub`() {
val block = block()
val reader = mock<ChainReader> {
on { read(ChainRequest("eth_getBlockByNumber", ListParams("latest", false))) } doReturn Mono.empty()
}
val wsSub = mock<WsSubscriptions> {
on { connectionInfoFlux() } doReturn Flux.empty()
on { subscribe(ChainRequest("eth_subscribe", ListParams("newHeads"))) } doReturn
WsSubscriptions.SubscribeData(Flux.just(Global.objectMapper.writeValueAsBytes(block)), "id", AtomicReference("subId"))
}
val connection = mock<WsConnection> {
on { isConnected } doReturn true
on { callRpc(ChainRequest("eth_chainId", ListParams())) } doReturn Mono.just(ChainResponse("\"0x1\"".toByteArray(), null))
on { callRpc(ChainRequest("net_version", ListParams())) } doReturn Mono.just(ChainResponse("\"1\"".toByteArray(), null))
}
val wsPool = mock<WsConnectionPool> {
on { getConnection() } doReturn connection
}
val wsClient = JsonRpcWsClient(wsPool)
val upstream = mock<DefaultUpstream> {
on { getId() } doReturn "id"
on { getChain() } doReturn Chain.ETHEREUM__MAINNET
on { getOptions() } doReturn ChainOptions.PartialOptions().buildOptions()
}
val wsHead = GenericWsHead(
AlwaysForkChoice(),
BlockValidator.ALWAYS_VALID,
reader,
wsSub,
Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific,
wsClient,
Duration.ofSeconds(60),
)
StepVerifier.create(wsHead.getFlux())
.then { wsHead.start() }
.expectNext(BlockContainer.from(block))
.thenCancel()
.verify(Duration.ofSeconds(1))
verify(connection).callRpc(ChainRequest("eth_chainId", ListParams()))
verify(connection).callRpc(ChainRequest("net_version", ListParams()))
verify(wsSub).subscribe(ChainRequest("eth_subscribe", ListParams("newHeads")))
}
@Test
fun `validate chain settings and then fatal error`() {
val reader = mock<ChainReader> {
on { read(ChainRequest("eth_getBlockByNumber", ListParams("latest", false))) } doReturn Mono.empty()
}
val wsSub = mock<WsSubscriptions> {
on { connectionInfoFlux() } doReturn Flux.empty()
}
val connection = mock<WsConnection> {
on { isConnected } doReturn true
on { callRpc(ChainRequest("eth_chainId", ListParams())) } doReturn Mono.just(ChainResponse("\"0x1\"".toByteArray(), null))
on { callRpc(ChainRequest("net_version", ListParams())) } doReturn Mono.just(ChainResponse("\"155\"".toByteArray(), null))
}
val wsPool = mock<WsConnectionPool> {
on { getConnection() } doReturn connection
}
val wsClient = JsonRpcWsClient(wsPool)
val upstream = mock<DefaultUpstream> {
on { getId() } doReturn "id"
on { getChain() } doReturn Chain.ETHEREUM__MAINNET
on { getOptions() } doReturn ChainOptions.PartialOptions().buildOptions()
}
val wsHead = GenericWsHead(
AlwaysForkChoice(),
BlockValidator.ALWAYS_VALID,
reader,
wsSub,
Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific,
wsClient,
Duration.ofSeconds(60),
)
StepVerifier.create(wsHead.headLiveness())
.then { wsHead.start() }
.expectNext(HeadLivenessState.FATAL_ERROR)
.thenCancel()
.verify(Duration.ofSeconds(3))
verify(connection).callRpc(ChainRequest("eth_chainId", ListParams()))
verify(connection).callRpc(ChainRequest("net_version", ListParams()))
verify(wsSub, never()).subscribe(ChainRequest("eth_subscribe", ListParams("newHeads")))
}
@Test
fun `no validate chain settings if it's disabled`() {
val block = block()
val reader = mock<ChainReader> {
on { read(ChainRequest("eth_getBlockByNumber", ListParams("latest", false))) } doReturn Mono.empty()
}
val wsSub = mock<WsSubscriptions> {
on { connectionInfoFlux() } doReturn Flux.empty()
on { subscribe(ChainRequest("eth_subscribe", ListParams("newHeads"))) } doReturn
WsSubscriptions.SubscribeData(Flux.just(Global.objectMapper.writeValueAsBytes(block)), "id", AtomicReference("subId"))
}
val connection = mock<WsConnection>()
val wsPool = mock<WsConnectionPool> {
on { getConnection() } doReturn connection
}
val wsClient = JsonRpcWsClient(wsPool)
val upstream = mock<DefaultUpstream> {
on { getId() } doReturn "id"
on { getChain() } doReturn Chain.ETHEREUM__MAINNET
on { getOptions() } doReturn ChainOptions.PartialOptions(disableUpstreamValidation = true).buildOptions()
}
val wsHead = GenericWsHead(
AlwaysForkChoice(),
BlockValidator.ALWAYS_VALID,
reader,
wsSub,
Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific,
wsClient,
Duration.ofSeconds(60),
)
StepVerifier.create(wsHead.getFlux())
.then { wsHead.start() }
.expectNext(BlockContainer.from(block))
.thenCancel()
.verify(Duration.ofSeconds(3))
verify(connection, never()).callRpc(ChainRequest("eth_chainId", ListParams()))
verify(connection, never()).callRpc(ChainRequest("net_version", ListParams()))
verify(wsSub).subscribe(ChainRequest("eth_subscribe", ListParams("newHeads")))
}
@Test
fun `validate chain settings, getting an error and then head sub`() {
val block = block()
val reader = mock<ChainReader> {
on { read(ChainRequest("eth_getBlockByNumber", ListParams("latest", false))) } doReturn Mono.empty()
}
val connectionInfoSink = Sinks.many().multicast().directBestEffort<WsConnection.ConnectionInfo>()
val wsSub = mock<WsSubscriptions> {
on { connectionInfoFlux() } doReturn connectionInfoSink.asFlux()
on { subscribe(ChainRequest("eth_subscribe", ListParams("newHeads"))) } doReturn
WsSubscriptions.SubscribeData(Flux.just(Global.objectMapper.writeValueAsBytes(block)), "id", AtomicReference("subId"))
}
val connection = mock<WsConnection> {
on { isConnected } doReturn true
on { callRpc(ChainRequest("eth_chainId", ListParams())) } doReturn Mono.error(RuntimeException("err")) doReturn Mono.just(ChainResponse("\"0x1\"".toByteArray(), null))
on { callRpc(ChainRequest("net_version", ListParams())) } doReturn Mono.just(ChainResponse("\"1\"".toByteArray(), null))
}
val wsPool = mock<WsConnectionPool> {
on { getConnection() } doReturn connection
}
val wsClient = JsonRpcWsClient(wsPool)
val upstream = mock<DefaultUpstream> {
on { getId() } doReturn "id"
on { getChain() } doReturn Chain.ETHEREUM__MAINNET
on { getOptions() } doReturn ChainOptions.PartialOptions().buildOptions()
}
val wsHead = GenericWsHead(
AlwaysForkChoice(),
BlockValidator.ALWAYS_VALID,
reader,
wsSub,
Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
upstream,
EthereumChainSpecific,
wsClient,
Duration.ofSeconds(60),
)
StepVerifier.create(wsHead.getFlux())
.then {
wsHead.start()
connectionInfoSink.tryEmitNext(WsConnection.ConnectionInfo("id", WsConnection.ConnectionState.CONNECTED))
}
.expectNoEvent(Duration.ofMillis(1500))
.then {
wsHead.onNoHeadUpdates()
}
.expectNext(BlockContainer.from(block))
.thenCancel()
.verify(Duration.ofSeconds(3))
verify(connection, times(2)).callRpc(ChainRequest("eth_chainId", ListParams()))
verify(connection, times(2)).callRpc(ChainRequest("net_version", ListParams()))
verify(wsSub).subscribe(ChainRequest("eth_subscribe", ListParams("newHeads")))
}
private fun block() =
BlockJson<TransactionRefJson>()
.apply {
number = 1500000L
uncles = emptyList()
totalDifficulty = BigInteger.ONE
parentHash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
}
}