Enriched merge head for ethereum subscription (#262)
This commit is contained in:
@@ -0,0 +1,94 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import com.google.common.cache.CacheBuilder
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.Lifecycle
|
||||||
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
|
import reactor.core.Disposable
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.publisher.Sinks
|
||||||
|
import reactor.core.scheduler.Scheduler
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class EnrichedMergedHead constructor(
|
||||||
|
private val sources: Iterable<Head>,
|
||||||
|
private val referenceHead: Head,
|
||||||
|
private val headScheduler: Scheduler,
|
||||||
|
private val api: Reader<BlockHash, BlockContainer>
|
||||||
|
) : Head, Lifecycle {
|
||||||
|
|
||||||
|
private val enrichedBlocks = CacheBuilder.newBuilder()
|
||||||
|
.maximumSize(10)
|
||||||
|
.build<BlockId, BlockContainer>()
|
||||||
|
private val enrichedPromises = CacheBuilder.newBuilder()
|
||||||
|
.maximumSize(10)
|
||||||
|
.build<BlockId, Sinks.One<BlockContainer>>()
|
||||||
|
private var cacheSub: Disposable? = null
|
||||||
|
|
||||||
|
private fun getEnrichBlockMono(id: BlockId): Mono<BlockContainer> {
|
||||||
|
val block = enrichedBlocks.getIfPresent(id)
|
||||||
|
return if (block != null) {
|
||||||
|
Mono.just(block)
|
||||||
|
} else {
|
||||||
|
enrichedPromises.get(id) {
|
||||||
|
Sinks.one()
|
||||||
|
}.asMono()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFlux(): Flux<BlockContainer> {
|
||||||
|
return referenceHead.getFlux().concatMap { block ->
|
||||||
|
if (block.enriched) {
|
||||||
|
Mono.just(block)
|
||||||
|
} else {
|
||||||
|
Mono.firstWithValue(
|
||||||
|
getEnrichBlockMono(block.hash),
|
||||||
|
Mono.just(block)
|
||||||
|
.delayElement(Duration.ofSeconds(1))
|
||||||
|
.flatMap {
|
||||||
|
EthereumBlockEnricher.enrich(BlockHash(block.hash.value), api, headScheduler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeforeBlock(handler: Runnable) {}
|
||||||
|
|
||||||
|
override fun getCurrentHeight(): Long? {
|
||||||
|
return referenceHead.getCurrentHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean {
|
||||||
|
return cacheSub != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
cacheSub?.dispose()
|
||||||
|
sources.forEach { head ->
|
||||||
|
if (head is Lifecycle && !head.isRunning()) {
|
||||||
|
head.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (referenceHead is Lifecycle && !referenceHead.isRunning()) {
|
||||||
|
referenceHead.start()
|
||||||
|
}
|
||||||
|
cacheSub = Flux.merge(sources.map { it.getFlux() }).subscribe { block ->
|
||||||
|
if (block.enriched) {
|
||||||
|
enrichedBlocks.put(block.hash, block)
|
||||||
|
enrichedPromises.get(block.hash) { Sinks.one() }.tryEmitValue(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
cacheSub?.dispose()
|
||||||
|
cacheSub = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSyncingNode(isSyncing: Boolean) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.scheduler.Scheduler
|
||||||
|
import reactor.retry.Repeat
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class EthereumBlockEnricher {
|
||||||
|
companion object {
|
||||||
|
fun enrich(blockHash: BlockHash, api: Reader<BlockHash, BlockContainer>, scheduler: Scheduler): Mono<BlockContainer> {
|
||||||
|
return Mono.just(blockHash)
|
||||||
|
.flatMap { hash ->
|
||||||
|
api.read(hash)
|
||||||
|
.subscribeOn(scheduler)
|
||||||
|
.timeout(Defaults.timeoutInternal, Mono.empty())
|
||||||
|
}.repeatWhenEmpty { n ->
|
||||||
|
Repeat.times<Any>(5)
|
||||||
|
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
|
||||||
|
.apply(n)
|
||||||
|
}
|
||||||
|
.timeout(Defaults.timeout, Mono.empty())
|
||||||
|
.onErrorResume { Mono.empty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ interface EthereumLikeMultistream : Upstream, HasEgressSubscription {
|
|||||||
|
|
||||||
fun getHead(mather: Selector.Matcher): Head
|
fun getHead(mather: Selector.Matcher): Head
|
||||||
|
|
||||||
|
fun getEnrichedHead(mather: Selector.Matcher): Head
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to proxy the native subscribe request to the managed upstreams if
|
* Tries to proxy the native subscribe request to the managed upstreams if
|
||||||
* - any of them matches the matcher criteria
|
* - any of them matches the matcher criteria
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
|||||||
import io.emeraldpay.dshackle.Chain
|
import io.emeraldpay.dshackle.Chain
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.reader.JsonRpcReader
|
import io.emeraldpay.dshackle.reader.JsonRpcReader
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.ChainFees
|
import io.emeraldpay.dshackle.upstream.ChainFees
|
||||||
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
|
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
|
||||||
import io.emeraldpay.dshackle.upstream.EgressSubscription
|
import io.emeraldpay.dshackle.upstream.EgressSubscription
|
||||||
@@ -38,6 +40,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
|
|||||||
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
|
||||||
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
import org.springframework.cloud.sleuth.Tracer
|
import org.springframework.cloud.sleuth.Tracer
|
||||||
import org.springframework.util.ConcurrentReferenceHashMap
|
import org.springframework.util.ConcurrentReferenceHashMap
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -209,6 +212,25 @@ open class EthereumMultistream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getEnrichedHead(mather: Selector.Matcher): Head =
|
||||||
|
filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
|
||||||
|
upstreams.filter { mather.matches(it) }
|
||||||
|
.apply {
|
||||||
|
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
|
||||||
|
}.let {
|
||||||
|
val selected = it.map { source -> source.getHead() }
|
||||||
|
EnrichedMergedHead(
|
||||||
|
selected, getHead(), headScheduler,
|
||||||
|
object :
|
||||||
|
Reader<BlockHash, BlockContainer> {
|
||||||
|
override fun read(key: BlockHash): Mono<BlockContainer> {
|
||||||
|
return reader.blocksByHashAsCont().read(key).map { res -> res.data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getFeeEstimation(): ChainFees {
|
override fun getFeeEstimation(): ChainFees {
|
||||||
return feeEstimation
|
return feeEstimation
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,24 +16,24 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.Defaults
|
|
||||||
import io.emeraldpay.dshackle.Global
|
import io.emeraldpay.dshackle.Global
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.reader.JsonRpcReader
|
import io.emeraldpay.dshackle.reader.JsonRpcReader
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.BlockValidator
|
import io.emeraldpay.dshackle.upstream.BlockValidator
|
||||||
import io.emeraldpay.dshackle.upstream.Lifecycle
|
import io.emeraldpay.dshackle.upstream.Lifecycle
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
|
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import reactor.retry.Repeat
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
class EthereumWsHead(
|
class EthereumWsHead(
|
||||||
@@ -101,7 +101,25 @@ class EthereumWsHead(
|
|||||||
block.totalDifficulty == null
|
block.totalDifficulty == null
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
enhanceRealBlock(block)
|
EthereumBlockEnricher.enrich(
|
||||||
|
block.hash,
|
||||||
|
object :
|
||||||
|
Reader<BlockHash, BlockContainer> {
|
||||||
|
override fun read(key: BlockHash): Mono<BlockContainer> {
|
||||||
|
return api.read(JsonRpcRequest("eth_getBlockByHash", listOf(block.hash.toHex(), false)))
|
||||||
|
.flatMap { resp ->
|
||||||
|
if (resp.isNull()) {
|
||||||
|
Mono.error(SilentException("Received null for block ${block.hash}"))
|
||||||
|
} else {
|
||||||
|
Mono.just(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map { BlockContainer.fromEthereumJson(it, upstreamId) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headScheduler
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Mono.just(BlockContainer.from(block))
|
Mono.just(BlockContainer.from(block))
|
||||||
}
|
}
|
||||||
@@ -113,30 +131,6 @@ class EthereumWsHead(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enhanceRealBlock(block: BlockJson<TransactionRefJson>): Mono<BlockContainer> {
|
|
||||||
return Mono.just(block.hash)
|
|
||||||
.flatMap { hash ->
|
|
||||||
api.read(JsonRpcRequest("eth_getBlockByHash", listOf(hash.toHex(), false)))
|
|
||||||
.flatMap { resp ->
|
|
||||||
if (resp.isNull()) {
|
|
||||||
Mono.error(SilentException("Received null for block $hash"))
|
|
||||||
} else {
|
|
||||||
Mono.just(resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.flatMap(JsonRpcResponse::requireResult)
|
|
||||||
.map { BlockContainer.fromEthereumJson(it, upstreamId) }
|
|
||||||
.subscribeOn(headScheduler)
|
|
||||||
.timeout(Defaults.timeoutInternal, Mono.empty())
|
|
||||||
}.repeatWhenEmpty { n ->
|
|
||||||
Repeat.times<Any>(5)
|
|
||||||
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
|
|
||||||
.apply(n)
|
|
||||||
}
|
|
||||||
.timeout(Defaults.timeout, Mono.empty())
|
|
||||||
.onErrorResume { Mono.empty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
super.stop()
|
super.stop()
|
||||||
cancelSub()
|
cancelSub()
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class ConnectBlockUpdates(
|
|||||||
fun connect() = connect(Selector.empty)
|
fun connect() = connect(Selector.empty)
|
||||||
override fun connect(matcher: Selector.Matcher): Flux<Update> {
|
override fun connect(matcher: Selector.Matcher): Flux<Update> {
|
||||||
return connected.computeIfAbsent(matcher.describeInternal()) { key ->
|
return connected.computeIfAbsent(matcher.describeInternal()) { key ->
|
||||||
extract(upstream.getHead(matcher))
|
extract(upstream.getEnrichedHead(matcher))
|
||||||
.publishOn(scheduler)
|
.publishOn(scheduler)
|
||||||
.publish()
|
.publish()
|
||||||
.refCount(1, Duration.ofSeconds(60))
|
.refCount(1, Duration.ofSeconds(60))
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
|||||||
import io.emeraldpay.dshackle.Chain
|
import io.emeraldpay.dshackle.Chain
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.reader.JsonRpcReader
|
import io.emeraldpay.dshackle.reader.JsonRpcReader
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.ChainFees
|
import io.emeraldpay.dshackle.upstream.ChainFees
|
||||||
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
|
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
|
||||||
import io.emeraldpay.dshackle.upstream.EmptyHead
|
import io.emeraldpay.dshackle.upstream.EmptyHead
|
||||||
@@ -36,6 +38,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
|
|||||||
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
|
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
|
||||||
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
import org.springframework.cloud.sleuth.Tracer
|
import org.springframework.cloud.sleuth.Tracer
|
||||||
import org.springframework.util.ConcurrentReferenceHashMap
|
import org.springframework.util.ConcurrentReferenceHashMap
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -179,6 +182,25 @@ open class EthereumPosMultiStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getEnrichedHead(mather: Selector.Matcher): Head =
|
||||||
|
filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
|
||||||
|
upstreams.filter { mather.matches(it) }
|
||||||
|
.apply {
|
||||||
|
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
|
||||||
|
}.let {
|
||||||
|
val selected = it.map { source -> source.getHead() }
|
||||||
|
EnrichedMergedHead(
|
||||||
|
selected, getHead(), headScheduler,
|
||||||
|
object :
|
||||||
|
Reader<BlockHash, BlockContainer> {
|
||||||
|
override fun read(key: BlockHash): Mono<BlockContainer> {
|
||||||
|
return reader.blocksByHashAsCont().read(key).map { res -> res.data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getFeeEstimation(): ChainFees {
|
override fun getFeeEstimation(): ChainFees {
|
||||||
return feeEstimation
|
return feeEstimation
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import io.emeraldpay.dshackle.FileResolver
|
|||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||||
import io.emeraldpay.dshackle.config.CacheConfig
|
import io.emeraldpay.dshackle.config.CacheConfig
|
||||||
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
|
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.data.BlockId
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
import io.emeraldpay.dshackle.reader.EmptyReader
|
import io.emeraldpay.dshackle.reader.EmptyReader
|
||||||
@@ -35,9 +34,12 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
|||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.etherjar.domain.BlockHash
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
||||||
|
import io.emeraldpay.etherjar.domain.TransactionId
|
||||||
|
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
|
||||||
import io.micrometer.core.instrument.MeterRegistry
|
import io.micrometer.core.instrument.MeterRegistry
|
||||||
import io.micrometer.core.instrument.logging.LoggingMeterRegistry
|
import io.micrometer.core.instrument.logging.LoggingMeterRegistry
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.bouncycastle.jcajce.provider.digest.Keccak
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -125,13 +127,25 @@ class TestingCommons {
|
|||||||
BlockJson block = new BlockJson().tap {
|
BlockJson block = new BlockJson().tap {
|
||||||
setNumber(height)
|
setNumber(height)
|
||||||
setParentHash(BlockHash.from("0xc4b01774e426325b50f0c709753ec7cf1f1774439d587dfb91f2a4eeb8179cde"))
|
setParentHash(BlockHash.from("0xc4b01774e426325b50f0c709753ec7cf1f1774439d587dfb91f2a4eeb8179cde"))
|
||||||
setHash(BlockHash.from("0xc4b01774e426325b50f0c709753ec7cf1f1774439d587dfb91f2a4eeb8179cde"))
|
setHash(BlockHash.from((new Keccak.Digest256()).digest(height.byteValue())))
|
||||||
setTotalDifficulty(BigInteger.ONE)
|
setTotalDifficulty(BigInteger.ONE)
|
||||||
setTimestamp(predictableTimestamp(height, 14))
|
setTimestamp(predictableTimestamp(height, 14))
|
||||||
}
|
}
|
||||||
return BlockContainer.from(block)
|
return BlockContainer.from(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static BlockContainer enrichedBlockForEthereum(Long height) {
|
||||||
|
BlockJson block = new BlockJson().tap {
|
||||||
|
setNumber(height)
|
||||||
|
setParentHash(BlockHash.from("0xc4b01774e426325b50f0c709753ec7cf1f1774439d587dfb91f2a4eeb8179cde"))
|
||||||
|
setHash(BlockHash.from((new Keccak.Digest256()).digest(height.byteValue())))
|
||||||
|
setTotalDifficulty(BigInteger.ONE)
|
||||||
|
setTimestamp(predictableTimestamp(height, 14))
|
||||||
|
setTransactions([new TransactionRefJson(TransactionId.from("0x3b23294ade15d39261245e6a3a53c3429a015891c95885b44ded29da2d60b29c"))])
|
||||||
|
}
|
||||||
|
return BlockContainer.from(block)
|
||||||
|
}
|
||||||
|
|
||||||
static BlockContainer blockForBitcoin(Long height) {
|
static BlockContainer blockForBitcoin(Long height) {
|
||||||
def parent = BlockId.from(StringUtils.leftPad(height.toString(), 64, "0"))
|
def parent = BlockId.from(StringUtils.leftPad(height.toString(), 64, "0"))
|
||||||
return new BlockContainer(
|
return new BlockContainer(
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.test.ApiReaderMock
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.dshackle.upstream.AbstractHead
|
||||||
|
import io.emeraldpay.dshackle.upstream.BlockValidator
|
||||||
|
import io.emeraldpay.dshackle.upstream.Lifecycle
|
||||||
|
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
|
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 spock.lang.Specification
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class EnrichedMergedHeadSpec extends Specification {
|
||||||
|
|
||||||
|
def "ensures that heads are running on start"() {
|
||||||
|
setup:
|
||||||
|
def head1 = Mock(TestHead) {
|
||||||
|
_ * isRunning() >> false
|
||||||
|
_ * getFlux() >> Flux.empty()
|
||||||
|
}
|
||||||
|
def head2 = Mock(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> Flux.empty()
|
||||||
|
}
|
||||||
|
def head3 = Mock(TestHead) {
|
||||||
|
_ * isRunning() >> false
|
||||||
|
_ * getFlux() >> Flux.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
def api = new ApiReaderMock()
|
||||||
|
when:
|
||||||
|
def merged = new EnrichedMergedHead([head1, head2], head3, Schedulers.parallel(), new BlockReader(api))
|
||||||
|
merged.start()
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * head3.start()
|
||||||
|
1 * head1.start()
|
||||||
|
merged.isRunning()
|
||||||
|
}
|
||||||
|
|
||||||
|
def "if enriched block comes from reference head we pass it along instantly"() {
|
||||||
|
setup:
|
||||||
|
def block = TestingCommons.enrichedBlockForEthereum(100)
|
||||||
|
def api = new ApiReaderMock()
|
||||||
|
def head = Stub(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> Flux.just(block)
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
def merge = new EnrichedMergedHead([], head, Schedulers.parallel(), new BlockReader(api))
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(merge.getFlux())
|
||||||
|
.then { merge.start() }
|
||||||
|
.expectNext(block)
|
||||||
|
.thenCancel()
|
||||||
|
.verify(Duration.ofMillis(100))
|
||||||
|
block.enriched
|
||||||
|
}
|
||||||
|
|
||||||
|
def "enriched block arrived in sources before reference block"() {
|
||||||
|
setup:
|
||||||
|
def enrichedBlock = TestingCommons.enrichedBlockForEthereum(100)
|
||||||
|
def block = TestingCommons.blockForEthereum(100)
|
||||||
|
Sinks.Many<BlockContainer> refSink = Sinks.many().multicast().directBestEffort()
|
||||||
|
def headRef = Stub(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> refSink.asFlux()
|
||||||
|
}
|
||||||
|
def headSource = Stub(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> Flux.just(enrichedBlock)
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
def merge = new EnrichedMergedHead([headSource], headRef, Schedulers.parallel(), new BlockReader(new ApiReaderMock()))
|
||||||
|
then:
|
||||||
|
StepVerifier.create(merge.getFlux())
|
||||||
|
.then { merge.start() }
|
||||||
|
.expectNoEvent(Duration.ofMillis(100))
|
||||||
|
.then { refSink.tryEmitNext(block) }
|
||||||
|
.expectNext(enrichedBlock)
|
||||||
|
.thenCancel()
|
||||||
|
.verify(Duration.ofMillis(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "enriched block arrived in source after reference block, but before deadline"() {
|
||||||
|
setup:
|
||||||
|
def enrichedBlock = TestingCommons.enrichedBlockForEthereum(100)
|
||||||
|
def block = TestingCommons.blockForEthereum(100)
|
||||||
|
Sinks.Many<BlockContainer> sourceSink = Sinks.many().multicast().directBestEffort()
|
||||||
|
def headRef = Stub(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> Flux.just(block)
|
||||||
|
}
|
||||||
|
def headSource = Stub(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> sourceSink.asFlux()
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
def merge = new EnrichedMergedHead([headSource], headRef, Schedulers.parallel(), new BlockReader(new ApiReaderMock()))
|
||||||
|
then:
|
||||||
|
StepVerifier.create(merge.getFlux())
|
||||||
|
.then { merge.start() }
|
||||||
|
.expectNoEvent(Duration.ofMillis(600))
|
||||||
|
.then { sourceSink.tryEmitNext(enrichedBlock) }
|
||||||
|
.expectNext(enrichedBlock)
|
||||||
|
.thenCancel()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "enriched blocks does not arrive before deadline"() {
|
||||||
|
setup:
|
||||||
|
def enrichedBlock = TestingCommons.enrichedBlockForEthereum(100)
|
||||||
|
def block = TestingCommons.blockForEthereum(100)
|
||||||
|
def headRef = Stub(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> Flux.just(block)
|
||||||
|
}
|
||||||
|
def headSource = Stub(TestHead) {
|
||||||
|
_ * isRunning() >> true
|
||||||
|
_ * getFlux() >> Flux.just(block)
|
||||||
|
}
|
||||||
|
def api = new ApiReaderMock().tap {
|
||||||
|
answer("eth_getBlockByHash", [block.hash.toHexWithPrefix(), false], enrichedBlock.toBlock())
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
def merge = new EnrichedMergedHead([headSource], headRef, Schedulers.parallel(), new BlockReader(api))
|
||||||
|
then:
|
||||||
|
StepVerifier.create(merge.getFlux())
|
||||||
|
.then { merge.start() }
|
||||||
|
.expectNoEvent(Duration.ofSeconds(1))
|
||||||
|
.expectNext(enrichedBlock)
|
||||||
|
.thenCancel()
|
||||||
|
.verify(Duration.ofMillis(1200))
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockReader implements Reader<BlockHash, BlockContainer> {
|
||||||
|
private ApiReaderMock mockApi
|
||||||
|
BlockReader(ApiReaderMock api) {
|
||||||
|
mockApi = api
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<BlockContainer> read(BlockHash hash) {
|
||||||
|
return mockApi.read(new JsonRpcRequest("eth_getBlockByHash", [hash.toHex(), false]))
|
||||||
|
.map {
|
||||||
|
def t = it
|
||||||
|
def a = 1
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map { BlockContainer.fromEthereumJson(it, "test") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestHead extends AbstractHead implements Lifecycle {
|
||||||
|
|
||||||
|
TestHead() {
|
||||||
|
super(new MostWorkForkChoice(), Schedulers.parallel(), new BlockValidator.AlwaysValid(), 100_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void start() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void stop() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean isRunning() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,7 +233,7 @@ class ConnectBlockUpdatesSpec extends Specification {
|
|||||||
1 * getFlux() >> Flux.never()
|
1 * getFlux() >> Flux.never()
|
||||||
}
|
}
|
||||||
def up = Mock(EthereumMultistream) {
|
def up = Mock(EthereumMultistream) {
|
||||||
1 * getHead(Selector.empty) >> head
|
1 * getEnrichedHead(Selector.empty) >> head
|
||||||
}
|
}
|
||||||
def connectBlockUpdates = new ConnectBlockUpdates(up, Schedulers.parallel())
|
def connectBlockUpdates = new ConnectBlockUpdates(up, Schedulers.parallel())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user