Track upstream finalization data (#499)
This commit is contained in:
Submodule emerald-grpc updated: a3351cdcb5...421724301b
@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.dshackle.Chain
|
import io.emeraldpay.dshackle.Chain
|
||||||
import io.emeraldpay.dshackle.Global
|
import io.emeraldpay.dshackle.Global
|
||||||
import io.emeraldpay.dshackle.Global.Companion.nullValue
|
import io.emeraldpay.dshackle.Global.Companion.nullValue
|
||||||
@@ -47,6 +48,7 @@ import io.emeraldpay.dshackle.upstream.Upstream
|
|||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.CallParams
|
import io.emeraldpay.dshackle.upstream.rpcclient.CallParams
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.ObjectParams
|
import io.emeraldpay.dshackle.upstream.rpcclient.ObjectParams
|
||||||
@@ -243,6 +245,12 @@ open class NativeCall(
|
|||||||
result.upstreamId = it.id
|
result.upstreamId = it.id
|
||||||
result.upstreamNodeVersion = it.nodeVersion
|
result.upstreamNodeVersion = it.nodeVersion
|
||||||
}
|
}
|
||||||
|
it.finalization?.let {
|
||||||
|
result.finalization = Common.FinalizationData.newBuilder()
|
||||||
|
.setHeight(it.height)
|
||||||
|
.setType(it.type.toProtoFinalizationType())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
return result.build()
|
return result.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,9 +434,9 @@ open class NativeCall(
|
|||||||
val resolvedUpstreamData = it.resolvedUpstreamData ?: ctx.upstream.getUpstreamSettingsData()
|
val resolvedUpstreamData = it.resolvedUpstreamData ?: ctx.upstream.getUpstreamSettingsData()
|
||||||
validateResult(result, "local", ctx)
|
validateResult(result, "local", ctx)
|
||||||
if (ctx.nonce != null) {
|
if (ctx.nonce != null) {
|
||||||
CallResult.ok(ctx.id, ctx.nonce, result, signer.sign(ctx.nonce, result, resolvedUpstreamData?.id ?: ctx.upstream.getId()), resolvedUpstreamData, ctx)
|
CallResult.ok(ctx.id, ctx.nonce, result, signer.sign(ctx.nonce, result, resolvedUpstreamData?.id ?: ctx.upstream.getId()), resolvedUpstreamData, ctx, it.finalization)
|
||||||
} else {
|
} else {
|
||||||
CallResult.ok(ctx.id, null, result, null, resolvedUpstreamData, ctx)
|
CallResult.ok(ctx.id, null, result, null, resolvedUpstreamData, ctx, it.finalization)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.switchIfEmpty(
|
}.switchIfEmpty(
|
||||||
@@ -436,6 +444,10 @@ open class NativeCall(
|
|||||||
)
|
)
|
||||||
.onErrorResume {
|
.onErrorResume {
|
||||||
Mono.just(CallResult.fail(ctx.id, ctx.nonce, it, ctx))
|
Mono.just(CallResult.fail(ctx.id, ctx.nonce, it, ctx))
|
||||||
|
}.doOnNext {
|
||||||
|
if (it.finalization != null && it.upstreamSettingsData != null) {
|
||||||
|
ctx.upstream.addFinalization(it.finalization, it.upstreamSettingsData.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +712,7 @@ open class NativeCall(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class CallResult(
|
open class CallResult @JvmOverloads constructor(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val nonce: Long?,
|
val nonce: Long?,
|
||||||
val result: ByteArray?,
|
val result: ByteArray?,
|
||||||
@@ -709,6 +721,7 @@ open class NativeCall(
|
|||||||
val upstreamSettingsData: Upstream.UpstreamSettingsData?,
|
val upstreamSettingsData: Upstream.UpstreamSettingsData?,
|
||||||
val ctx: ValidCallContext<ParsedCallDetails>?,
|
val ctx: ValidCallContext<ParsedCallDetails>?,
|
||||||
val stream: Flux<Chunk>? = null,
|
val stream: Flux<Chunk>? = null,
|
||||||
|
val finalization: FinalizationData? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -725,6 +738,10 @@ open class NativeCall(
|
|||||||
return CallResult(id, nonce, result, null, signature, upstreamSettingsData, ctx)
|
return CallResult(id, nonce, result, null, signature, upstreamSettingsData, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ok(id: Int, nonce: Long?, result: ByteArray, signature: ResponseSigner.Signature?, upstreamSettingsData: Upstream.UpstreamSettingsData?, ctx: ValidCallContext<ParsedCallDetails>?, final: FinalizationData?): CallResult {
|
||||||
|
return CallResult(id, nonce, result, null, signature, upstreamSettingsData, ctx, null, final)
|
||||||
|
}
|
||||||
|
|
||||||
fun ok(id: Int, nonce: Long?, result: ByteArray, signature: ResponseSigner.Signature?, upstreamSettingsData: Upstream.UpstreamSettingsData?, ctx: ValidCallContext<ParsedCallDetails>?, stream: Flux<Chunk>?): CallResult {
|
fun ok(id: Int, nonce: Long?, result: ByteArray, signature: ResponseSigner.Signature?, upstreamSettingsData: Upstream.UpstreamSettingsData?, ctx: ValidCallContext<ParsedCallDetails>?, stream: Flux<Chunk>?): CallResult {
|
||||||
return CallResult(id, nonce, result, null, signature, upstreamSettingsData, ctx, stream)
|
return CallResult(id, nonce, result, null, signature, upstreamSettingsData, ctx, stream)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import reactor.core.publisher.Mono
|
|||||||
class StreamHead(
|
class StreamHead(
|
||||||
@Autowired private val multistreamHolder: MultistreamHolder,
|
@Autowired private val multistreamHolder: MultistreamHolder,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(StreamHead::class.java)
|
private val log = LoggerFactory.getLogger(StreamHead::class.java)
|
||||||
|
|
||||||
fun add(requestMono: Mono<Common.Chain>): Flux<BlockchainOuterClass.ChainHead> {
|
fun add(requestMono: Mono<Common.Chain>): Flux<BlockchainOuterClass.ChainHead> {
|
||||||
@@ -54,12 +53,20 @@ class StreamHead(
|
|||||||
|
|
||||||
fun asProto(ms: Multistream, chain: Chain, block: BlockContainer): BlockchainOuterClass.ChainHead {
|
fun asProto(ms: Multistream, chain: Chain, block: BlockContainer): BlockchainOuterClass.ChainHead {
|
||||||
val msLowerBounds = ms.getLowerBounds()
|
val msLowerBounds = ms.getLowerBounds()
|
||||||
val lowerBoundsProto = msLowerBounds
|
val lowerBoundsProto =
|
||||||
.map {
|
msLowerBounds
|
||||||
BlockchainOuterClass.LowerBound.newBuilder()
|
.map {
|
||||||
.setLowerBoundTimestamp(it.timestamp)
|
BlockchainOuterClass.LowerBound.newBuilder()
|
||||||
.setLowerBoundType(toProtoLowerBoundType(it.type))
|
.setLowerBoundTimestamp(it.timestamp)
|
||||||
.setLowerBoundValue(it.lowerBound)
|
.setLowerBoundType(toProtoLowerBoundType(it.type))
|
||||||
|
.setLowerBoundValue(it.lowerBound)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
val finalizationData =
|
||||||
|
ms.getFinalizations().map {
|
||||||
|
Common.FinalizationData.newBuilder()
|
||||||
|
.setHeight(it.height)
|
||||||
|
.setType(it.type.toProtoFinalizationType())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
val toOldApi = toOldApi(msLowerBounds)
|
val toOldApi = toOldApi(msLowerBounds)
|
||||||
@@ -72,6 +79,7 @@ class StreamHead(
|
|||||||
.setCurrentLowerSlot(toOldApi.slot)
|
.setCurrentLowerSlot(toOldApi.slot)
|
||||||
.setCurrentLowerDataTimestamp(toOldApi.timestamp)
|
.setCurrentLowerDataTimestamp(toOldApi.timestamp)
|
||||||
.addAllLowerBounds(lowerBoundsProto)
|
.addAllLowerBounds(lowerBoundsProto)
|
||||||
|
.addAllFinalizationData(finalizationData)
|
||||||
.setTimestamp(block.timestamp.toEpochMilli())
|
.setTimestamp(block.timestamp.toEpochMilli())
|
||||||
.setWeight(ByteString.copyFrom(block.difficulty.toByteArray()))
|
.setWeight(ByteString.copyFrom(block.difficulty.toByteArray()))
|
||||||
.setBlockId(block.hash.toHex())
|
.setBlockId(block.hash.toHex())
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ open class GenericUpstreamCreator(
|
|||||||
cs::upstreamRpcModulesDetector,
|
cs::upstreamRpcModulesDetector,
|
||||||
buildMethodsFun,
|
buildMethodsFun,
|
||||||
cs::lowerBoundService,
|
cs::lowerBoundService,
|
||||||
|
cs::finalizationDetectorBuilder,
|
||||||
)
|
)
|
||||||
|
|
||||||
upstream.start()
|
upstream.start()
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ package io.emeraldpay.dshackle.upstream
|
|||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer
|
import com.fasterxml.jackson.databind.JsonSerializer
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider
|
import com.fasterxml.jackson.databind.SerializerProvider
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import io.emeraldpay.dshackle.upstream.stream.Chunk
|
import io.emeraldpay.dshackle.upstream.stream.Chunk
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
class ChainResponse(
|
class ChainResponse @JvmOverloads constructor(
|
||||||
private val result: ByteArray?,
|
private val result: ByteArray?,
|
||||||
val error: ChainCallError?,
|
val error: ChainCallError?,
|
||||||
val id: Id,
|
val id: Id,
|
||||||
@@ -33,15 +34,18 @@ class ChainResponse(
|
|||||||
*/
|
*/
|
||||||
val providedSignature: ResponseSigner.Signature? = null,
|
val providedSignature: ResponseSigner.Signature? = null,
|
||||||
val resolvedUpstreamData: Upstream.UpstreamSettingsData? = null,
|
val resolvedUpstreamData: Upstream.UpstreamSettingsData? = null,
|
||||||
|
val finalization: FinalizationData? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(stream: Flux<Chunk>, id: Int) :
|
constructor(stream: Flux<Chunk>, id: Int) :
|
||||||
this(null, null, NumberId(id.toLong()), stream, null, null)
|
this(null, null, NumberId(id.toLong()), stream, null, null, null)
|
||||||
|
|
||||||
constructor(result: ByteArray?, error: ChainCallError?) : this(result, error, NumberId(0), null)
|
constructor(result: ByteArray?, error: ChainCallError?) : this(result, error, NumberId(0), null, null)
|
||||||
|
|
||||||
constructor(result: ByteArray?, error: ChainCallError?, resolvedUpstreamData: Upstream.UpstreamSettingsData?) :
|
constructor(result: ByteArray?, error: ChainCallError?, resolvedUpstreamData: Upstream.UpstreamSettingsData?) :
|
||||||
this(result, error, NumberId(0), null, null, resolvedUpstreamData)
|
this(result, error, NumberId(0), null, null, resolvedUpstreamData, null)
|
||||||
|
constructor(result: ByteArray?, resolvedUpstreamData: Upstream.UpstreamSettingsData?, finalization: FinalizationData) :
|
||||||
|
this(result, null, NumberId(0), null, null, resolvedUpstreamData, finalization)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val NULL_VALUE = "null".toByteArray()
|
private val NULL_VALUE = "null".toByteArray()
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
|||||||
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallSelector
|
import io.emeraldpay.dshackle.upstream.calls.CallSelector
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
import io.micrometer.core.instrument.Gauge
|
import io.micrometer.core.instrument.Gauge
|
||||||
@@ -48,6 +50,7 @@ import reactor.core.publisher.Sinks
|
|||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregation of multiple upstreams responding to a single blockchain
|
* Aggregation of multiple upstreams responding to a single blockchain
|
||||||
@@ -355,6 +358,18 @@ abstract class Multistream(
|
|||||||
started = true
|
started = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFinalizations(): Collection<FinalizationData> {
|
||||||
|
return getAll().flatMap { it.getFinalizations() }
|
||||||
|
.fold(mutableMapOf<FinalizationType, Long>()) { acc, data ->
|
||||||
|
acc[data.type] = max(acc[data.type] ?: 0, data.height)
|
||||||
|
acc
|
||||||
|
}.toList().map { FinalizationData(it.second, it.first) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addFinalization(finalization: FinalizationData, upstreamId: String) {
|
||||||
|
getAll().find { it.getId() == upstreamId }?.addFinalization(finalization, upstreamId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLowerBounds(): Collection<LowerBoundData> {
|
override fun getLowerBounds(): Collection<LowerBoundData> {
|
||||||
return lowerBounds.values
|
return lowerBounds.values
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import io.emeraldpay.dshackle.upstream.MatchesResponse.NotMatchedResponse
|
|||||||
import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse
|
import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse
|
||||||
import io.emeraldpay.dshackle.upstream.MatchesResponse.SlotHeightResponse
|
import io.emeraldpay.dshackle.upstream.MatchesResponse.SlotHeightResponse
|
||||||
import io.emeraldpay.dshackle.upstream.MatchesResponse.Success
|
import io.emeraldpay.dshackle.upstream.MatchesResponse.Success
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.fromProtoType
|
import io.emeraldpay.dshackle.upstream.lowerbound.fromProtoType
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
@@ -41,6 +42,51 @@ class Selector {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
val anyLabel = AnyLabelMatcher()
|
val anyLabel = AnyLabelMatcher()
|
||||||
|
|
||||||
|
sealed class HeightNumberOrTag {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromHeightSelector(selector: BlockchainOuterClass.HeightSelector): HeightNumberOrTag? {
|
||||||
|
return when (selector.heightOrNumberCase) {
|
||||||
|
BlockchainOuterClass.HeightSelector.HeightOrNumberCase.HEIGHTORNUMBER_NOT_SET ->
|
||||||
|
return if (selector.height == -1L) {
|
||||||
|
Latest
|
||||||
|
} else {
|
||||||
|
Number(selector.height)
|
||||||
|
}
|
||||||
|
BlockchainOuterClass.HeightSelector.HeightOrNumberCase.NUMBER -> Number(selector.number)
|
||||||
|
BlockchainOuterClass.HeightSelector.HeightOrNumberCase.TAG -> when (selector.tag) {
|
||||||
|
BlockchainOuterClass.BlockTag.SAFE -> Safe
|
||||||
|
BlockchainOuterClass.BlockTag.LATEST -> Latest
|
||||||
|
BlockchainOuterClass.BlockTag.PENDING -> Pending
|
||||||
|
BlockchainOuterClass.BlockTag.FINALIZED -> Finalized
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Number(val num: Long) : HeightNumberOrTag()
|
||||||
|
object Pending : HeightNumberOrTag()
|
||||||
|
object Latest : HeightNumberOrTag()
|
||||||
|
object Safe : HeightNumberOrTag()
|
||||||
|
object Finalized : HeightNumberOrTag()
|
||||||
|
|
||||||
|
fun getSort(): Sort {
|
||||||
|
return when (this) {
|
||||||
|
is Latest -> Sort(
|
||||||
|
compareByDescending {
|
||||||
|
it.getHead().getCurrentHeight()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
is Safe -> Sort.safe
|
||||||
|
is Finalized -> Sort.finalized
|
||||||
|
|
||||||
|
else -> Sort.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun convertToUpstreamFilter(selectors: List<BlockchainOuterClass.Selector>): UpstreamFilter {
|
fun convertToUpstreamFilter(selectors: List<BlockchainOuterClass.Selector>): UpstreamFilter {
|
||||||
val matcher = selectors
|
val matcher = selectors
|
||||||
@@ -50,15 +96,9 @@ class Selector {
|
|||||||
SlotMatcher(it.slotHeightSelector.slotHeight)
|
SlotMatcher(it.slotHeightSelector.slotHeight)
|
||||||
}
|
}
|
||||||
it.hasHeightSelector() -> {
|
it.hasHeightSelector() -> {
|
||||||
val height = if (it.heightSelector.height == -1L) {
|
when (val selector = HeightNumberOrTag.fromHeightSelector(it.heightSelector)) {
|
||||||
null
|
is HeightNumberOrTag.Number -> HeightMatcher(selector.num)
|
||||||
} else {
|
else -> empty
|
||||||
it.heightSelector.height
|
|
||||||
}
|
|
||||||
if (height == null) {
|
|
||||||
empty
|
|
||||||
} else {
|
|
||||||
HeightMatcher(height)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> empty
|
else -> empty
|
||||||
@@ -71,8 +111,8 @@ class Selector {
|
|||||||
|
|
||||||
private fun getSort(selectors: List<BlockchainOuterClass.Selector>): Sort {
|
private fun getSort(selectors: List<BlockchainOuterClass.Selector>): Sort {
|
||||||
selectors.forEach { selector ->
|
selectors.forEach { selector ->
|
||||||
if (selector.hasHeightSelector() && selector.heightSelector.height == -1L) {
|
if (selector.hasHeightSelector()) {
|
||||||
return Sort(compareByDescending { it.getHead().getCurrentHeight() })
|
return HeightNumberOrTag.fromHeightSelector(selector.heightSelector)?.getSort() ?: Sort.default
|
||||||
} else if (selector.hasLowerHeightSelector()) {
|
} else if (selector.hasLowerHeightSelector()) {
|
||||||
return Sort(
|
return Sort(
|
||||||
compareBy(nullsLast()) {
|
compareBy(nullsLast()) {
|
||||||
@@ -180,6 +220,16 @@ class Selector {
|
|||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val default = Sort(compareBy { null })
|
val default = Sort(compareBy { null })
|
||||||
|
val safe = Sort(
|
||||||
|
compareByDescending { up ->
|
||||||
|
up.getFinalizations().find { it.type == FinalizationType.SAFE_BLOCK }?.height ?: 0L
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val finalized = Sort(
|
||||||
|
compareByDescending { up ->
|
||||||
|
up.getFinalizations().find { it.type == FinalizationType.FINALIZED_BLOCK }?.height ?: 0L
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.foundation.ChainOptions
|
|||||||
import io.emeraldpay.dshackle.reader.ChainReader
|
import io.emeraldpay.dshackle.reader.ChainReader
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -49,6 +50,8 @@ interface Upstream : Lifecycle {
|
|||||||
fun isGrpc(): Boolean
|
fun isGrpc(): Boolean
|
||||||
fun getLowerBounds(): Collection<LowerBoundData>
|
fun getLowerBounds(): Collection<LowerBoundData>
|
||||||
fun getLowerBound(lowerBoundType: LowerBoundType): LowerBoundData?
|
fun getLowerBound(lowerBoundType: LowerBoundType): LowerBoundData?
|
||||||
|
fun getFinalizations(): Collection<FinalizationData>
|
||||||
|
fun addFinalization(finalization: FinalizationData, upstreamId: String)
|
||||||
fun getUpstreamSettingsData(): UpstreamSettingsData?
|
fun getUpstreamSettingsData(): UpstreamSettingsData?
|
||||||
fun updateLowerBound(lowerBound: Long, type: LowerBoundType)
|
fun updateLowerBound(lowerBound: Long, type: LowerBoundType)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import io.emeraldpay.dshackle.upstream.Lifecycle
|
|||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
@@ -81,6 +82,13 @@ open class BitcoinRpcUpstream(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFinalizations(): Collection<FinalizationData> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addFinalization(finalization: FinalizationData, upstreamId: String) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun getUpstreamSettingsData(): Upstream.UpstreamSettingsData? {
|
override fun getUpstreamSettingsData(): Upstream.UpstreamSettingsData? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import io.emeraldpay.dshackle.reader.CompoundReader
|
|||||||
import io.emeraldpay.dshackle.reader.Reader
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.reader.RekeyingReader
|
import io.emeraldpay.dshackle.reader.RekeyingReader
|
||||||
import io.emeraldpay.dshackle.reader.SpannedReader
|
import io.emeraldpay.dshackle.reader.SpannedReader
|
||||||
import io.emeraldpay.dshackle.reader.TransformingReader
|
|
||||||
import io.emeraldpay.dshackle.upstream.CachingReader
|
import io.emeraldpay.dshackle.upstream.CachingReader
|
||||||
import io.emeraldpay.dshackle.upstream.Multistream
|
import io.emeraldpay.dshackle.upstream.Multistream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
@@ -45,6 +44,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
|||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
|
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionLogJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionLogJson
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import org.apache.commons.collections4.Factory
|
import org.apache.commons.collections4.Factory
|
||||||
import org.springframework.cloud.sleuth.Tracer
|
import org.springframework.cloud.sleuth.Tracer
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
@@ -92,18 +92,8 @@ open class EthereumCachingReader(
|
|||||||
SpannedReader(RekeyingReader(idToBlockHash, directReader.blockReader), tracer, DIRECT_QUORUM_RPC_READER),
|
SpannedReader(RekeyingReader(idToBlockHash, directReader.blockReader), tracer, DIRECT_QUORUM_RPC_READER),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun blocksByHashAsCont(): Reader<BlockHash, Result<BlockContainer>> {
|
open fun blockByFinalization(): Reader<FinalizationType, Result<BlockContainer>> {
|
||||||
return CompoundReader(
|
return SpannedReader(directReader.blockByFinalizationReader, tracer, DIRECT_QUORUM_RPC_READER)
|
||||||
SpannedReader(CacheWithUpstreamIdReader(RekeyingReader(blockHashToId, caches.getBlocksByHash())), tracer, CACHE_BLOCK_BY_HASH_READER),
|
|
||||||
SpannedReader(directReader.blockReader, tracer, DIRECT_QUORUM_RPC_READER),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun blocksByHashParsed(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
|
||||||
return TransformingReader(
|
|
||||||
blocksByHashAsCont(),
|
|
||||||
extractBlock,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun blocksByIdAsCont(): Reader<BlockId, Result<BlockContainer>> {
|
open fun blocksByIdAsCont(): Reader<BlockId, Result<BlockContainer>> {
|
||||||
@@ -117,23 +107,6 @@ open class EthereumCachingReader(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun blocksByHeightParsed(): Reader<Long, BlockJson<TransactionRefJson>> {
|
|
||||||
return TransformingReader(
|
|
||||||
blocksByHeightAsCont(),
|
|
||||||
extractBlock,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun txByHash(): Reader<TransactionId, TransactionJsonSnapshot> {
|
|
||||||
return TransformingReader(
|
|
||||||
CompoundReader(
|
|
||||||
CacheWithUpstreamIdReader(RekeyingReader(txHashToId, caches.getTxByHash())),
|
|
||||||
directReader.txReader,
|
|
||||||
),
|
|
||||||
extractTx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun logsByHash(): Reader<BlockId, Result<List<TransactionLogJson>>> {
|
open fun logsByHash(): Reader<BlockId, Result<List<TransactionLogJson>>> {
|
||||||
return directReader.logsByHashReader
|
return directReader.logsByHashReader
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.subscribe.AggregatedPendingTxes
|
|||||||
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumWsIngressSubscription
|
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumWsIngressSubscription
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
|
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.finalization.FinalizationDetector
|
||||||
import io.emeraldpay.dshackle.upstream.generic.AbstractPollChainSpecific
|
import io.emeraldpay.dshackle.upstream.generic.AbstractPollChainSpecific
|
||||||
import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder
|
import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder
|
||||||
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
|
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
|
||||||
@@ -101,7 +102,14 @@ object EthereumChainSpecific : AbstractPollChainSpecific() {
|
|||||||
return EthereumLowerBoundService(chain, upstream)
|
return EthereumLowerBoundService(chain, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun upstreamSettingsDetector(chain: Chain, upstream: Upstream): UpstreamSettingsDetector {
|
override fun finalizationDetectorBuilder(): FinalizationDetector {
|
||||||
|
return EthereumFinalizationDetector()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun upstreamSettingsDetector(
|
||||||
|
chain: Chain,
|
||||||
|
upstream: Upstream,
|
||||||
|
): UpstreamSettingsDetector {
|
||||||
return EthereumUpstreamSettingsDetector(upstream, chain)
|
return EthereumUpstreamSettingsDetector(upstream, chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionReceiptJson
|
|||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||||
import org.apache.commons.collections4.Factory
|
import org.apache.commons.collections4.Factory
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
@@ -66,6 +67,7 @@ class EthereumDirectReader(
|
|||||||
val balanceReader: Reader<Address, Result<Wei>>
|
val balanceReader: Reader<Address, Result<Wei>>
|
||||||
val receiptReader: Reader<TransactionId, Result<ByteArray>>
|
val receiptReader: Reader<TransactionId, Result<ByteArray>>
|
||||||
val logsByHashReader: Reader<BlockId, Result<List<TransactionLogJson>>>
|
val logsByHashReader: Reader<BlockId, Result<List<TransactionLogJson>>>
|
||||||
|
val blockByFinalizationReader: Reader<FinalizationType, Result<BlockContainer>>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
blockReader = object : Reader<BlockHash, Result<BlockContainer>> {
|
blockReader = object : Reader<BlockHash, Result<BlockContainer>> {
|
||||||
@@ -105,6 +107,24 @@ class EthereumDirectReader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockByFinalizationReader = object : Reader<FinalizationType, Result<BlockContainer>> {
|
||||||
|
override fun read(key: FinalizationType): Mono<Result<BlockContainer>> {
|
||||||
|
val request = ChainRequest("eth_getBlockByNumber", ListParams(key.toBlockRef(), false))
|
||||||
|
val tag = when (key) {
|
||||||
|
FinalizationType.FINALIZED_BLOCK -> Selector.Companion.HeightNumberOrTag.Finalized
|
||||||
|
FinalizationType.SAFE_BLOCK -> Selector.Companion.HeightNumberOrTag.Safe
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return readBlock(
|
||||||
|
request,
|
||||||
|
key.toString(),
|
||||||
|
Selector.empty,
|
||||||
|
tag?.getSort() ?: Selector.Sort.default,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
balanceReader = object : Reader<Address, Result<Wei>> {
|
balanceReader = object : Reader<Address, Result<Wei>> {
|
||||||
override fun read(key: Address): Mono<Result<Wei>> {
|
override fun read(key: Address): Mono<Result<Wei>> {
|
||||||
val height = up.getHead().getCurrentHeight()?.let { HexQuantity.from(it).toHex() } ?: "latest"
|
val height = up.getHead().getCurrentHeight()?.let { HexQuantity.from(it).toHex() } ?: "latest"
|
||||||
@@ -194,8 +214,9 @@ class EthereumDirectReader(
|
|||||||
request: ChainRequest,
|
request: ChainRequest,
|
||||||
id: String,
|
id: String,
|
||||||
matcher: Selector.Matcher = Selector.empty,
|
matcher: Selector.Matcher = Selector.empty,
|
||||||
|
sort: Selector.Sort = Selector.Sort.default,
|
||||||
): Mono<Result<BlockContainer>> {
|
): Mono<Result<BlockContainer>> {
|
||||||
return readWithQuorum(request, matcher)
|
return readWithQuorum(request, matcher, sort)
|
||||||
.timeout(Duration.ofSeconds(5), Mono.error(TimeoutException("Block not read $id")))
|
.timeout(Duration.ofSeconds(5), Mono.error(TimeoutException("Block not read $id")))
|
||||||
.retryWhen(Retry.fixedDelay(3, Duration.ofMillis(200)))
|
.retryWhen(Retry.fixedDelay(3, Duration.ofMillis(200)))
|
||||||
.flatMap { result ->
|
.flatMap { result ->
|
||||||
@@ -227,6 +248,7 @@ class EthereumDirectReader(
|
|||||||
private fun readWithQuorum(
|
private fun readWithQuorum(
|
||||||
request: ChainRequest,
|
request: ChainRequest,
|
||||||
matcher: Selector.Matcher = Selector.empty,
|
matcher: Selector.Matcher = Selector.empty,
|
||||||
|
sort: Selector.Sort = Selector.Sort.default,
|
||||||
): Mono<Result<ByteArray>> {
|
): Mono<Result<ByteArray>> {
|
||||||
return Mono.just(requestReaderFactory)
|
return Mono.just(requestReaderFactory)
|
||||||
.map {
|
.map {
|
||||||
@@ -237,7 +259,7 @@ class EthereumDirectReader(
|
|||||||
it.create(
|
it.create(
|
||||||
RequestReaderFactory.ReaderData(
|
RequestReaderFactory.ReaderData(
|
||||||
up,
|
up,
|
||||||
Selector.UpstreamFilter(requestMatcher),
|
Selector.UpstreamFilter(sort, requestMatcher),
|
||||||
callMethodsFactory.create().createQuorumFor(request.method),
|
callMethodsFactory.create().createQuorumFor(request.method),
|
||||||
null,
|
null,
|
||||||
tracer,
|
tracer,
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Global
|
||||||
|
import io.emeraldpay.dshackle.upstream.ChainRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationDetector
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class EthereumFinalizationDetector : FinalizationDetector {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(EthereumFinalizationDetector::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
val data: ConcurrentHashMap<FinalizationType, FinalizationData> = ConcurrentHashMap()
|
||||||
|
|
||||||
|
override fun detectFinalization(
|
||||||
|
upstream: Upstream,
|
||||||
|
blockTime: Duration,
|
||||||
|
): Flux<FinalizationData> {
|
||||||
|
val timer =
|
||||||
|
Flux.merge(
|
||||||
|
Flux.just(1),
|
||||||
|
Flux.interval(blockTime.coerceAtLeast(Duration.ofSeconds(1)).multipliedBy(6)),
|
||||||
|
)
|
||||||
|
return timer.flatMap {
|
||||||
|
Flux.fromIterable(
|
||||||
|
listOf(
|
||||||
|
Pair(
|
||||||
|
FinalizationType.SAFE_BLOCK,
|
||||||
|
ChainRequest(
|
||||||
|
"eth_getBlockByNumber",
|
||||||
|
ListParams("safe", false),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
FinalizationType.FINALIZED_BLOCK,
|
||||||
|
ChainRequest(
|
||||||
|
"eth_getBlockByNumber",
|
||||||
|
ListParams("finalized", false),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).flatMap { (type, req) ->
|
||||||
|
upstream
|
||||||
|
.getIngressReader()
|
||||||
|
.read(req)
|
||||||
|
.flatMap {
|
||||||
|
it.requireResult().map { result ->
|
||||||
|
val block =
|
||||||
|
Global.objectMapper
|
||||||
|
.readValue(result, BlockJson::class.java) as BlockJson<TransactionRefJson>?
|
||||||
|
if (block != null) {
|
||||||
|
FinalizationData(block.number, type)
|
||||||
|
} else {
|
||||||
|
throw RpcException(RpcResponseError.CODE_INVALID_JSON, "can't parse block data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.doOnNext {
|
||||||
|
addFinalization(it)
|
||||||
|
}.onErrorResume {
|
||||||
|
log.error("Error during retrieving — $it")
|
||||||
|
Flux.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addFinalization(finalization: FinalizationData) {
|
||||||
|
data[finalization.type] = maxOf(data[finalization.type], finalization) { a, b ->
|
||||||
|
((a?.height ?: 0) - (b?.height ?: 0)).toInt()
|
||||||
|
} ?: finalization
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFinalizations(): Collection<FinalizationData> {
|
||||||
|
return data.values
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,11 +23,12 @@ import io.emeraldpay.dshackle.upstream.ChainRequest
|
|||||||
import io.emeraldpay.dshackle.upstream.ChainResponse
|
import io.emeraldpay.dshackle.upstream.ChainResponse
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.LogsOracle
|
import io.emeraldpay.dshackle.upstream.LogsOracle
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.hex.HexQuantity
|
import io.emeraldpay.dshackle.upstream.ethereum.hex.HexQuantity
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.kotlin.core.publisher.switchIfEmpty
|
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||||
@@ -59,12 +60,11 @@ class EthereumLocalReader(
|
|||||||
// we do not want to serve any requests (except hardcoded) that have nonces from cache
|
// we do not want to serve any requests (except hardcoded) that have nonces from cache
|
||||||
return Mono.empty()
|
return Mono.empty()
|
||||||
}
|
}
|
||||||
val common = commonRequests(key)
|
return commonRequests(key)?.switchIfEmpty {
|
||||||
?.switchIfEmpty { Mono.just(nullValue to null) }
|
// we need to explicitly return null to prevent executeOnRemote
|
||||||
if (common != null) {
|
// for example
|
||||||
return common.map { ChainResponse(it.first, null, it.second) }
|
Mono.just(ChainResponse(nullValue, null, null))
|
||||||
}
|
} ?: Mono.empty()
|
||||||
return Mono.empty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +72,7 @@ class EthereumLocalReader(
|
|||||||
* parses JSON into Map. But the purpose of further processing and caching for some of the requests we want
|
* parses JSON into Map. But the purpose of further processing and caching for some of the requests we want
|
||||||
* to have actual data types.
|
* to have actual data types.
|
||||||
*/
|
*/
|
||||||
fun commonRequests(key: ChainRequest): Mono<Pair<ByteArray, Upstream.UpstreamSettingsData?>>? {
|
fun commonRequests(key: ChainRequest): Mono<ChainResponse>? {
|
||||||
val method = key.method
|
val method = key.method
|
||||||
val params = key.params
|
val params = key.params
|
||||||
if (params is ListParams) {
|
if (params is ListParams) {
|
||||||
@@ -89,7 +89,7 @@ class EthereumLocalReader(
|
|||||||
}
|
}
|
||||||
reader.txByHashAsCont()
|
reader.txByHashAsCont()
|
||||||
.read(hash)
|
.read(hash)
|
||||||
.map { it.data.json!! to it.resolvedUpstreamData }
|
.map { ChainResponse(it.data.json, null, it.resolvedUpstreamData) }
|
||||||
}
|
}
|
||||||
|
|
||||||
method == "eth_getBlockByHash" -> {
|
method == "eth_getBlockByHash" -> {
|
||||||
@@ -106,7 +106,9 @@ class EthereumLocalReader(
|
|||||||
if (withTx) {
|
if (withTx) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
reader.blocksByIdAsCont().read(hash).map { it.data.json!! to it.resolvedUpstreamData }
|
reader.blocksByIdAsCont().read(hash).map {
|
||||||
|
ChainResponse(it.data.json, null, it.resolvedUpstreamData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ class EthereumLocalReader(
|
|||||||
}
|
}
|
||||||
reader.receipts()
|
reader.receipts()
|
||||||
.read(hash)
|
.read(hash)
|
||||||
.map { it.data to it.resolvedUpstreamData }
|
.map { ChainResponse(it.data, null, it.resolvedUpstreamData) }
|
||||||
}
|
}
|
||||||
|
|
||||||
method == "drpc_getLogsEstimate" -> {
|
method == "drpc_getLogsEstimate" -> {
|
||||||
@@ -139,7 +141,7 @@ class EthereumLocalReader(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBlockByNumber(params: List<Any?>): Mono<Pair<ByteArray, Upstream.UpstreamSettingsData?>>? {
|
fun getBlockByNumber(params: List<Any?>): Mono<ChainResponse>? {
|
||||||
if (params.size != 2 || params[0] == null || params[1] == null) {
|
if (params.size != 2 || params[0] == null || params[1] == null) {
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters")
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters")
|
||||||
}
|
}
|
||||||
@@ -168,9 +170,17 @@ class EthereumLocalReader(
|
|||||||
blockRef == "earliest" -> {
|
blockRef == "earliest" -> {
|
||||||
number = 0
|
number = 0
|
||||||
}
|
}
|
||||||
blockRef == "finalized" || blockRef == "safe" || blockRef == "pending" -> {
|
blockRef == "pending" -> {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
blockRef == "finalized" || blockRef == "safe" -> {
|
||||||
|
val type = FinalizationType.fromBlockRef(blockRef)
|
||||||
|
return reader
|
||||||
|
.blockByFinalization().read(type)
|
||||||
|
.map {
|
||||||
|
ChainResponse(it.data.json, it.resolvedUpstreamData, FinalizationData(it.data.height, type))
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Block number is invalid")
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Block number is invalid")
|
||||||
}
|
}
|
||||||
@@ -180,10 +190,10 @@ class EthereumLocalReader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return reader.blocksByHeightAsCont()
|
return reader.blocksByHeightAsCont()
|
||||||
.read(number).map { it.data.json!! to it.resolvedUpstreamData }
|
.read(number).map { ChainResponse(it.data.json, null, it.resolvedUpstreamData) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLogsEstimate(params: List<Any?>): Mono<Pair<ByteArray, Upstream.UpstreamSettingsData?>>? {
|
fun getLogsEstimate(params: List<Any?>): Mono<ChainResponse>? {
|
||||||
if (logsOracle == null) {
|
if (logsOracle == null) {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
@@ -234,7 +244,7 @@ class EthereumLocalReader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return logsOracle.estimate(limit?.toLong(), fromBlock, toBlock, address, topics)
|
return logsOracle.estimate(limit?.toLong(), fromBlock, toBlock, address, topics)
|
||||||
.map { it.toByteArray() to null }
|
.map { ChainResponse(it.toByteArray(), null, null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseBlockRef(blockRef: String?): Long {
|
private fun parseBlockRef(blockRef: String?): Long {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.finalization
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
|
||||||
|
class FinalizationData(
|
||||||
|
val height: Long,
|
||||||
|
val type: FinalizationType,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "FinalizationData($height, ${type.toBlockRef()})"
|
||||||
|
}
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return when (other) {
|
||||||
|
is FinalizationData -> other.height == height && other.type == type
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FinalizationType {
|
||||||
|
UNKNOWN,
|
||||||
|
SAFE_BLOCK,
|
||||||
|
FINALIZED_BLOCK,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromBlockRef(v: String): FinalizationType {
|
||||||
|
return when (v) {
|
||||||
|
"safe" -> SAFE_BLOCK
|
||||||
|
"finalized" -> FINALIZED_BLOCK
|
||||||
|
else -> UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toProtoFinalizationType(): Common.FinalizationType {
|
||||||
|
return when (this) {
|
||||||
|
FINALIZED_BLOCK -> Common.FinalizationType.FINALIZATION_FINALIZED_BLOCK
|
||||||
|
UNKNOWN -> Common.FinalizationType.UNRECOGNIZED
|
||||||
|
SAFE_BLOCK -> Common.FinalizationType.FINALIZATION_SAFE_BLOCK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toBlockRef(): String {
|
||||||
|
return when (this) {
|
||||||
|
FINALIZED_BLOCK -> "finalized"
|
||||||
|
SAFE_BLOCK -> "safe"
|
||||||
|
UNKNOWN -> "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Common.FinalizationType.fromProtoType(): FinalizationType {
|
||||||
|
return when (this) {
|
||||||
|
Common.FinalizationType.FINALIZATION_UNSPECIFIED -> FinalizationType.UNKNOWN
|
||||||
|
Common.FinalizationType.FINALIZATION_SAFE_BLOCK -> FinalizationType.SAFE_BLOCK
|
||||||
|
Common.FinalizationType.FINALIZATION_FINALIZED_BLOCK -> FinalizationType.FINALIZED_BLOCK
|
||||||
|
Common.FinalizationType.UNRECOGNIZED -> FinalizationType.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.finalization
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
interface FinalizationDetector {
|
||||||
|
fun detectFinalization(
|
||||||
|
upstream: Upstream,
|
||||||
|
blockTime: Duration,
|
||||||
|
): Flux<FinalizationData>
|
||||||
|
|
||||||
|
fun getFinalizations(): Collection<FinalizationData>
|
||||||
|
|
||||||
|
fun addFinalization(finalization: FinalizationData)
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoopFinalizationDetector : FinalizationDetector {
|
||||||
|
override fun detectFinalization(
|
||||||
|
upstream: Upstream,
|
||||||
|
blockTime: Duration,
|
||||||
|
): Flux<FinalizationData> {
|
||||||
|
return Flux.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFinalizations(): Collection<FinalizationData> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addFinalization(finalization: FinalizationData) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,12 +20,13 @@ import io.emeraldpay.dshackle.upstream.UpstreamSettingsDetector
|
|||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallSelector
|
import io.emeraldpay.dshackle.upstream.calls.CallSelector
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
|
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationDetector
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.NoopFinalizationDetector
|
||||||
import org.springframework.cloud.sleuth.Tracer
|
import org.springframework.cloud.sleuth.Tracer
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
|
|
||||||
abstract class AbstractChainSpecific : ChainSpecific {
|
abstract class AbstractChainSpecific : ChainSpecific {
|
||||||
|
|
||||||
override fun localReaderBuilder(
|
override fun localReaderBuilder(
|
||||||
cachingReader: CachingReader,
|
cachingReader: CachingReader,
|
||||||
methods: CallMethods,
|
methods: CallMethods,
|
||||||
@@ -35,11 +36,18 @@ abstract class AbstractChainSpecific : ChainSpecific {
|
|||||||
return Mono.just(LocalReader(methods))
|
return Mono.just(LocalReader(methods))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun finalizationDetectorBuilder(): FinalizationDetector {
|
||||||
|
return NoopFinalizationDetector()
|
||||||
|
}
|
||||||
|
|
||||||
override fun makeCachingReaderBuilder(tracer: Tracer): CachingReaderBuilder {
|
override fun makeCachingReaderBuilder(tracer: Tracer): CachingReaderBuilder {
|
||||||
return { _, _, _ -> NoopCachingReader }
|
return { _, _, _ -> NoopCachingReader }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun upstreamSettingsDetector(chain: Chain, upstream: Upstream): UpstreamSettingsDetector? {
|
override fun upstreamSettingsDetector(
|
||||||
|
chain: Chain,
|
||||||
|
upstream: Upstream,
|
||||||
|
): UpstreamSettingsDetector? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import io.emeraldpay.dshackle.upstream.calls.CallSelector
|
|||||||
import io.emeraldpay.dshackle.upstream.cosmos.CosmosChainSpecific
|
import io.emeraldpay.dshackle.upstream.cosmos.CosmosChainSpecific
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
|
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationDetector
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService
|
||||||
import io.emeraldpay.dshackle.upstream.near.NearChainSpecific
|
import io.emeraldpay.dshackle.upstream.near.NearChainSpecific
|
||||||
import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific
|
import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific
|
||||||
@@ -45,6 +46,7 @@ import reactor.core.scheduler.Scheduler
|
|||||||
typealias SubscriptionBuilder = (Multistream) -> EgressSubscription
|
typealias SubscriptionBuilder = (Multistream) -> EgressSubscription
|
||||||
typealias LocalReaderBuilder = (CachingReader, CallMethods, Head, LogsOracle?) -> Mono<ChainReader>
|
typealias LocalReaderBuilder = (CachingReader, CallMethods, Head, LogsOracle?) -> Mono<ChainReader>
|
||||||
typealias CachingReaderBuilder = (Multistream, Caches, Factory<CallMethods>) -> CachingReader
|
typealias CachingReaderBuilder = (Multistream, Caches, Factory<CallMethods>) -> CachingReader
|
||||||
|
typealias FinalizationDetectorBuilder = () -> FinalizationDetector
|
||||||
|
|
||||||
interface ChainSpecific {
|
interface ChainSpecific {
|
||||||
fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono<BlockContainer>
|
fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono<BlockContainer>
|
||||||
@@ -55,7 +57,14 @@ interface ChainSpecific {
|
|||||||
|
|
||||||
fun unsubscribeNewHeadsRequest(subId: String): ChainRequest
|
fun unsubscribeNewHeadsRequest(subId: String): ChainRequest
|
||||||
|
|
||||||
fun localReaderBuilder(cachingReader: CachingReader, methods: CallMethods, head: Head, logsOracle: LogsOracle?): Mono<ChainReader>
|
fun finalizationDetectorBuilder(): FinalizationDetector
|
||||||
|
|
||||||
|
fun localReaderBuilder(
|
||||||
|
cachingReader: CachingReader,
|
||||||
|
methods: CallMethods,
|
||||||
|
head: Head,
|
||||||
|
logsOracle: LogsOracle?,
|
||||||
|
): Mono<ChainReader>
|
||||||
|
|
||||||
fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription
|
fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.upstream.UpstreamValidator
|
|||||||
import io.emeraldpay.dshackle.upstream.UpstreamValidatorBuilder
|
import io.emeraldpay.dshackle.upstream.UpstreamValidatorBuilder
|
||||||
import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult
|
import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
import io.emeraldpay.dshackle.upstream.generic.connectors.ConnectorFactory
|
import io.emeraldpay.dshackle.upstream.generic.connectors.ConnectorFactory
|
||||||
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector
|
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
@@ -43,11 +44,12 @@ open class GenericUpstream(
|
|||||||
role: UpstreamsConfig.UpstreamRole,
|
role: UpstreamsConfig.UpstreamRole,
|
||||||
targets: CallMethods?,
|
targets: CallMethods?,
|
||||||
private val node: QuorumForLabels.QuorumItem?,
|
private val node: QuorumForLabels.QuorumItem?,
|
||||||
chainConfig: ChainsConfig.ChainConfig,
|
private val chainConfig: ChainsConfig.ChainConfig,
|
||||||
connectorFactory: ConnectorFactory,
|
connectorFactory: ConnectorFactory,
|
||||||
validatorBuilder: UpstreamValidatorBuilder,
|
validatorBuilder: UpstreamValidatorBuilder,
|
||||||
upstreamSettingsDetectorBuilder: UpstreamSettingsDetectorBuilder,
|
upstreamSettingsDetectorBuilder: UpstreamSettingsDetectorBuilder,
|
||||||
lowerBoundServiceBuilder: LowerBoundServiceBuilder,
|
lowerBoundServiceBuilder: LowerBoundServiceBuilder,
|
||||||
|
finalizationDetectorBuilder: FinalizationDetectorBuilder,
|
||||||
) : DefaultUpstream(id, hash, null, UpstreamAvailability.OK, options, role, targets, node, chainConfig, chain), Lifecycle {
|
) : DefaultUpstream(id, hash, null, UpstreamAvailability.OK, options, role, targets, node, chainConfig, chain), Lifecycle {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -63,7 +65,8 @@ open class GenericUpstream(
|
|||||||
upstreamRpcModulesDetectorBuilder: UpstreamRpcModulesDetectorBuilder,
|
upstreamRpcModulesDetectorBuilder: UpstreamRpcModulesDetectorBuilder,
|
||||||
buildMethods: (UpstreamsConfig.Upstream<*>, Chain) -> CallMethods,
|
buildMethods: (UpstreamsConfig.Upstream<*>, Chain) -> CallMethods,
|
||||||
lowerBoundServiceBuilder: LowerBoundServiceBuilder,
|
lowerBoundServiceBuilder: LowerBoundServiceBuilder,
|
||||||
) : this(config.id!!, chain, hash, options, config.role, buildMethods(config, chain), node, chainConfig, connectorFactory, validatorBuilder, upstreamSettingsDetectorBuilder, lowerBoundServiceBuilder) {
|
finalizationDetectorBuilder: FinalizationDetectorBuilder,
|
||||||
|
) : this(config.id!!, chain, hash, options, config.role, buildMethods(config, chain), node, chainConfig, connectorFactory, validatorBuilder, upstreamSettingsDetectorBuilder, lowerBoundServiceBuilder, finalizationDetectorBuilder) {
|
||||||
rpcModulesDetector = upstreamRpcModulesDetectorBuilder(this)
|
rpcModulesDetector = upstreamRpcModulesDetectorBuilder(this)
|
||||||
detectRpcModules(config, buildMethods)
|
detectRpcModules(config, buildMethods)
|
||||||
}
|
}
|
||||||
@@ -85,6 +88,9 @@ open class GenericUpstream(
|
|||||||
private val isUpstreamValid = AtomicBoolean(false)
|
private val isUpstreamValid = AtomicBoolean(false)
|
||||||
private val clientVersion = AtomicReference(UNKNOWN_CLIENT_VERSION)
|
private val clientVersion = AtomicReference(UNKNOWN_CLIENT_VERSION)
|
||||||
|
|
||||||
|
private val finalizationDetector = finalizationDetectorBuilder()
|
||||||
|
private var finalizationDetectorSubscription: Disposable? = null
|
||||||
|
|
||||||
override fun getHead(): Head {
|
override fun getHead(): Head {
|
||||||
return connector.getHead()
|
return connector.getHead()
|
||||||
}
|
}
|
||||||
@@ -255,6 +261,8 @@ open class GenericUpstream(
|
|||||||
detectSettings()
|
detectSettings()
|
||||||
|
|
||||||
detectLowerBlock()
|
detectLowerBlock()
|
||||||
|
|
||||||
|
detectFinalization()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
@@ -272,6 +280,8 @@ open class GenericUpstream(
|
|||||||
livenessSubscription = null
|
livenessSubscription = null
|
||||||
lowerBlockDetectorSubscription?.dispose()
|
lowerBlockDetectorSubscription?.dispose()
|
||||||
lowerBlockDetectorSubscription = null
|
lowerBlockDetectorSubscription = null
|
||||||
|
finalizationDetectorSubscription?.dispose()
|
||||||
|
finalizationDetectorSubscription = null
|
||||||
connector.getHead().stop()
|
connector.getHead().stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,13 +292,31 @@ open class GenericUpstream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun detectLowerBlock() {
|
override fun getFinalizations(): Collection<FinalizationData> {
|
||||||
lowerBlockDetectorSubscription = lowerBoundService.detectLowerBounds()
|
return finalizationDetector.getFinalizations()
|
||||||
.subscribe {
|
}
|
||||||
|
|
||||||
|
override fun addFinalization(finalization: FinalizationData, upstreamId: String) {
|
||||||
|
if (getId() == upstreamId) {
|
||||||
|
finalizationDetector.addFinalization(finalization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detectFinalization() {
|
||||||
|
finalizationDetectorSubscription =
|
||||||
|
finalizationDetector.detectFinalization(this, chainConfig.expectedBlockTime).subscribe {
|
||||||
sendUpstreamStateEvent(UPDATED)
|
sendUpstreamStateEvent(UPDATED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun detectLowerBlock() {
|
||||||
|
lowerBlockDetectorSubscription =
|
||||||
|
lowerBoundService.detectLowerBounds()
|
||||||
|
.subscribe {
|
||||||
|
sendUpstreamStateEvent(UPDATED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getIngressSubscription(): IngressSubscription {
|
fun getIngressSubscription(): IngressSubscription {
|
||||||
return connector.getIngressSubscription()
|
return connector.getIngressSubscription()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
|||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
@@ -155,6 +156,13 @@ class BitcoinGrpcUpstream(
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFinalizations(): Collection<FinalizationData> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addFinalization(finalization: FinalizationData, upstreamId: String) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLowerBound(lowerBoundType: LowerBoundType): LowerBoundData? {
|
override fun getLowerBound(lowerBoundType: LowerBoundType): LowerBoundData? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ import io.emeraldpay.dshackle.upstream.Lifecycle
|
|||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
|
import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.fromProtoType
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
@@ -74,52 +77,65 @@ open class GenericGrpcUpstream(
|
|||||||
),
|
),
|
||||||
GrpcUpstream,
|
GrpcUpstream,
|
||||||
Lifecycle {
|
Lifecycle {
|
||||||
|
private val blockConverter: Function<BlockchainOuterClass.ChainHead, GrpcHead.GrpcHeadData> =
|
||||||
private val blockConverter: Function<BlockchainOuterClass.ChainHead, GrpcHead.GrpcHeadData> = Function { value ->
|
Function { value ->
|
||||||
val parentHash =
|
val parentHash =
|
||||||
if (value.parentBlockId.isBlank()) {
|
if (value.parentBlockId.isBlank()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
BlockId.from(BlockHash.from("0x" + value.parentBlockId))
|
BlockId.from(BlockHash.from("0x" + value.parentBlockId))
|
||||||
}
|
}
|
||||||
val block = BlockContainer(
|
val block =
|
||||||
value.height,
|
BlockContainer(
|
||||||
BlockId.from(BlockHash.from("0x" + value.blockId)),
|
value.height,
|
||||||
BigInteger(1, value.weight.toByteArray()),
|
BlockId.from(BlockHash.from("0x" + value.blockId)),
|
||||||
Instant.ofEpochMilli(value.timestamp),
|
BigInteger(1, value.weight.toByteArray()),
|
||||||
false,
|
Instant.ofEpochMilli(value.timestamp),
|
||||||
null,
|
false,
|
||||||
null,
|
null,
|
||||||
parentHash,
|
null,
|
||||||
)
|
parentHash,
|
||||||
val lowerBounds = value.lowerBoundsList
|
)
|
||||||
.map { LowerBoundData(it.lowerBoundValue, it.lowerBoundTimestamp, it.lowerBoundType.fromProtoType()) }
|
val lowerBounds =
|
||||||
GrpcHead.GrpcHeadData(block, lowerBounds)
|
value.lowerBoundsList
|
||||||
}
|
.map { LowerBoundData(it.lowerBoundValue, it.lowerBoundTimestamp, it.lowerBoundType.fromProtoType()) }
|
||||||
|
val finalizationData =
|
||||||
|
value.finalizationDataList.map {
|
||||||
|
FinalizationData(it.height, it.type.fromProtoType())
|
||||||
|
}
|
||||||
|
GrpcHead.GrpcHeadData(block, lowerBounds, finalizationData)
|
||||||
|
}
|
||||||
|
|
||||||
private val upstreamStatus = GrpcUpstreamStatus(overrideLabels)
|
private val upstreamStatus = GrpcUpstreamStatus(overrideLabels)
|
||||||
private val grpcHead = GrpcHead(
|
private val grpcHead =
|
||||||
getId(),
|
GrpcHead(
|
||||||
chain,
|
getId(),
|
||||||
this,
|
chain,
|
||||||
remote,
|
this,
|
||||||
blockConverter,
|
remote,
|
||||||
null,
|
blockConverter,
|
||||||
NoChoiceWithPriorityForkChoice(nodeRating, parentId),
|
null,
|
||||||
headScheduler,
|
NoChoiceWithPriorityForkChoice(nodeRating, parentId),
|
||||||
)
|
headScheduler,
|
||||||
|
)
|
||||||
private var capabilities: Set<Capability> = emptySet()
|
private var capabilities: Set<Capability> = emptySet()
|
||||||
private val buildInfo: BuildInfo = BuildInfo()
|
private val buildInfo: BuildInfo = BuildInfo()
|
||||||
|
|
||||||
private val defaultReader: ChainReader = client.getReader()
|
private val defaultReader: ChainReader = client.getReader()
|
||||||
|
|
||||||
private val lowerBounds = ConcurrentHashMap<LowerBoundType, LowerBoundData>()
|
private val lowerBounds = ConcurrentHashMap<LowerBoundType, LowerBoundData>()
|
||||||
|
private var finalizationData = ConcurrentHashMap<FinalizationType, FinalizationData>()
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
grpcHead.lowerBoundsFlux()
|
grpcHead.rawDataFlux()
|
||||||
.publishOn(lowerBoundScheduler)
|
.publishOn(rawDataScheduler)
|
||||||
.subscribe {
|
.subscribe { head ->
|
||||||
lowerBounds[it.type] = it
|
head.lowerBounds.forEach {
|
||||||
|
lowerBounds[it.type] = it
|
||||||
|
}
|
||||||
|
head.finalizationData.forEach {
|
||||||
|
finalizationData[it.type] = it
|
||||||
|
}
|
||||||
sendUpstreamStateEvent(UpstreamChangeEvent.ChangeType.UPDATED)
|
sendUpstreamStateEvent(UpstreamChangeEvent.ChangeType.UPDATED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,6 +221,14 @@ open class GenericGrpcUpstream(
|
|||||||
return lowerBounds[lowerBoundType]
|
return lowerBounds[lowerBoundType]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFinalizations(): Collection<FinalizationData> {
|
||||||
|
return finalizationData.values
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addFinalization(finalization: FinalizationData, upstreamId: String) {
|
||||||
|
finalizationData[finalization.type] = finalization
|
||||||
|
}
|
||||||
|
|
||||||
override fun getUpstreamSettingsData(): Upstream.UpstreamSettingsData? {
|
override fun getUpstreamSettingsData(): Upstream.UpstreamSettingsData? {
|
||||||
return Upstream.UpstreamSettingsData(
|
return Upstream.UpstreamSettingsData(
|
||||||
nodeId(),
|
nodeId(),
|
||||||
@@ -214,8 +238,9 @@ open class GenericGrpcUpstream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val lowerBoundScheduler: Scheduler = Schedulers.fromExecutorService(
|
val rawDataScheduler: Scheduler =
|
||||||
Executors.newFixedThreadPool(4, CustomizableThreadFactory("grpc-lower-bound-")),
|
Schedulers.fromExecutorService(
|
||||||
)
|
Executors.newFixedThreadPool(4, CustomizableThreadFactory("grpc-raw-data-bound-")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import io.emeraldpay.dshackle.upstream.AbstractHead
|
|||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.Lifecycle
|
import io.emeraldpay.dshackle.upstream.Lifecycle
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.micrometer.core.instrument.Counter
|
import io.micrometer.core.instrument.Counter
|
||||||
@@ -54,10 +55,9 @@ class GrpcHead(
|
|||||||
private val forkChoice: ForkChoice,
|
private val forkChoice: ForkChoice,
|
||||||
headScheduler: Scheduler,
|
headScheduler: Scheduler,
|
||||||
) : AbstractHead(forkChoice, headScheduler, upstreamId = id), Lifecycle {
|
) : AbstractHead(forkChoice, headScheduler, upstreamId = id), Lifecycle {
|
||||||
|
|
||||||
private var headSubscription: Disposable? = null
|
private var headSubscription: Disposable? = null
|
||||||
|
|
||||||
private val lowerBoundsSink = Sinks.many().multicast().directBestEffort<LowerBoundData>()
|
private val rawDataSink = Sinks.many().multicast().directBestEffort<GrpcHeadData>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a new head subscription with connection to the remote
|
* Initiate a new head subscription with connection to the remote
|
||||||
@@ -92,14 +92,15 @@ class GrpcHead(
|
|||||||
log.warn("Head subscription finished: $it")
|
log.warn("Head subscription finished: $it")
|
||||||
}
|
}
|
||||||
|
|
||||||
var blocks = heads.map(converter)
|
var blocks =
|
||||||
.doOnNext {
|
heads.map(converter)
|
||||||
it.lowerBounds.forEach { bound -> lowerBoundsSink.tryEmitNext(bound) }
|
.doOnNext {
|
||||||
}
|
rawDataSink.tryEmitNext(it)
|
||||||
.map { it.block }
|
}
|
||||||
.distinctUntilChanged {
|
.map { it.block }
|
||||||
it.hash
|
.distinctUntilChanged {
|
||||||
}.filter { forkChoice.filter(it) }
|
it.hash
|
||||||
|
}.filter { forkChoice.filter(it) }
|
||||||
|
|
||||||
if (enhancer != null) {
|
if (enhancer != null) {
|
||||||
blocks = blocks.flatMap(enhancer)
|
blocks = blocks.flatMap(enhancer)
|
||||||
@@ -133,7 +134,7 @@ class GrpcHead(
|
|||||||
headSubscription?.dispose()
|
headSubscription?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lowerBoundsFlux(): Flux<LowerBoundData> = lowerBoundsSink.asFlux()
|
fun rawDataFlux(): Flux<GrpcHeadData> = rawDataSink.asFlux()
|
||||||
|
|
||||||
val headsCounter = Counter.builder("grpc_head_received")
|
val headsCounter = Counter.builder("grpc_head_received")
|
||||||
.tag("upstream", id)
|
.tag("upstream", id)
|
||||||
@@ -143,7 +144,8 @@ class GrpcHead(
|
|||||||
data class GrpcHeadData(
|
data class GrpcHeadData(
|
||||||
val block: BlockContainer,
|
val block: BlockContainer,
|
||||||
val lowerBounds: List<LowerBoundData>,
|
val lowerBounds: List<LowerBoundData>,
|
||||||
|
val finalizationData: List<FinalizationData>,
|
||||||
) {
|
) {
|
||||||
constructor(block: BlockContainer) : this(block, emptyList())
|
constructor(block: BlockContainer) : this(block, emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class ApiReaderMock implements Reader<ChainRequest, ChainResponse> {
|
|||||||
}
|
}
|
||||||
error = new ChainCallError(-32601, "Method ${request.method} with ${request.params} is not mocked")
|
error = new ChainCallError(-32601, "Method ${request.method} with ${request.params} is not mocked")
|
||||||
}
|
}
|
||||||
return new ChainResponse(result, error, ChainResponse.Id.from(request.id), null, null, null)
|
return new ChainResponse(result, error, ChainResponse.Id.from(request.id), null, null, null, null)
|
||||||
} as Callable<ChainResponse>
|
} as Callable<ChainResponse>
|
||||||
return Mono.fromCallable(call)
|
return Mono.fromCallable(call)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class GenericUpstreamMock extends GenericUpstream {
|
|||||||
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&validator,
|
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&validator,
|
||||||
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&upstreamSettingsDetector,
|
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&upstreamSettingsDetector,
|
||||||
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&lowerBoundService,
|
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&lowerBoundService,
|
||||||
|
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&finalizationDetectorBuilder,
|
||||||
)
|
)
|
||||||
this.ethereumHeadMock = this.getHead() as EthereumHeadMock
|
this.ethereumHeadMock = this.getHead() as EthereumHeadMock
|
||||||
setLag(0)
|
setLag(0)
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ class FilteredApisSpec extends Specification {
|
|||||||
connectorFactory,
|
connectorFactory,
|
||||||
cs.&validator,
|
cs.&validator,
|
||||||
cs.&upstreamSettingsDetector,
|
cs.&upstreamSettingsDetector,
|
||||||
cs.&lowerBoundService
|
cs.&lowerBoundService,
|
||||||
|
cs.&finalizationDetectorBuilder
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
def matcher = new Selector.LabelMatcher("test", ["foo"])
|
def matcher = new Selector.LabelMatcher("test", ["foo"])
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import io.emeraldpay.dshackle.test.TestingCommons
|
|||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.domain.Address
|
import io.emeraldpay.dshackle.upstream.ethereum.domain.Address
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
|
import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
|
||||||
@@ -33,6 +34,40 @@ class EthereumDirectReaderSpec extends Specification {
|
|||||||
String hash1 = "0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5"
|
String hash1 = "0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5"
|
||||||
String address1 = "0xe0aadb0a012dbcdc529c4c743d3e0385a0b54d3d"
|
String address1 = "0xe0aadb0a012dbcdc529c4c743d3e0385a0b54d3d"
|
||||||
Upstream.UpstreamSettingsData data = new Upstream.UpstreamSettingsData("test")
|
Upstream.UpstreamSettingsData data = new Upstream.UpstreamSettingsData("test")
|
||||||
|
def "Reads block by finalization"() {
|
||||||
|
setup:
|
||||||
|
def json = new BlockJson().tap {
|
||||||
|
number = 100
|
||||||
|
hash = BlockHash.from(hash1)
|
||||||
|
timestamp = Instant.now()
|
||||||
|
totalDifficulty = BigInteger.ONE
|
||||||
|
parentHash = BlockHash.from(hash1)
|
||||||
|
transactions = []
|
||||||
|
}
|
||||||
|
def calls = Mock(Factory) {
|
||||||
|
1 * create() >> new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET, false)
|
||||||
|
}
|
||||||
|
EthereumDirectReader reader = new EthereumDirectReader(
|
||||||
|
Stub(Multistream), Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
|
||||||
|
)
|
||||||
|
reader.requestReaderFactory = Mock(RequestReaderFactory) {
|
||||||
|
1 * create({ it.upstreamFilter.sort == Selector.Sort.safe }) >> Mock(RequestReader) {
|
||||||
|
1 * read(new ChainRequest("eth_getBlockByNumber", new ListParams(["safe", false]))) >> Mono.just(
|
||||||
|
new RequestReader.Result(
|
||||||
|
Global.objectMapper.writeValueAsBytes(json), null, 1, data, null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
def act = reader.blockByFinalizationReader.read(FinalizationType.SAFE_BLOCK)
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNextMatches { block ->
|
||||||
|
block.data.hash.toHexWithPrefix() == hash1
|
||||||
|
}
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
def "Reads block by hash"() {
|
def "Reads block by hash"() {
|
||||||
setup:
|
setup:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import io.emeraldpay.dshackle.upstream.EmptyHead
|
|||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ChainRequest
|
import io.emeraldpay.dshackle.upstream.ChainRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
||||||
import org.apache.commons.collections4.functors.ConstantFactory
|
import org.apache.commons.collections4.functors.ConstantFactory
|
||||||
@@ -84,8 +86,8 @@ class EthereumLocalReaderSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act != null
|
act != null
|
||||||
with(act.block()) {
|
with(act.block()) {
|
||||||
it.first.length > 0
|
it.result.length > 0
|
||||||
with(Global.objectMapper.readValue(it.first, BlockJson)) {
|
with(Global.objectMapper.readValue(it.result, BlockJson)) {
|
||||||
number == 101
|
number == 101
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,8 +114,8 @@ class EthereumLocalReaderSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act != null
|
act != null
|
||||||
with(act.block()) {
|
with(act.block()) {
|
||||||
it.first.length > 0
|
it.result.length > 0
|
||||||
with(Global.objectMapper.readValue(it.first, BlockJson)) {
|
with(Global.objectMapper.readValue(it.result, BlockJson)) {
|
||||||
number == 0
|
number == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,13 +142,43 @@ class EthereumLocalReaderSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act != null
|
act != null
|
||||||
with(act.block()) {
|
with(act.block()) {
|
||||||
it.first.length > 0
|
it.result.length > 0
|
||||||
with(Global.objectMapper.readValue(it.first, BlockJson)) {
|
with(Global.objectMapper.readValue(it.result, BlockJson)) {
|
||||||
number == 74735
|
number == 74735
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def "getBlockByNumber fetches the block by tag"() {
|
||||||
|
setup:
|
||||||
|
def head = Stub(Head) {}
|
||||||
|
def reader = Mock(EthereumCachingReader) {
|
||||||
|
1 * blockByFinalization() >> Mock(Reader) {
|
||||||
|
1 * read(FinalizationType.SAFE_BLOCK) >> Mono.just(
|
||||||
|
new EthereumDirectReader.Result<>(TestingCommons.blockForEthereum(74735L), null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET, false)
|
||||||
|
def router = new EthereumLocalReader(reader, methods, head, null)
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = router.read(
|
||||||
|
new ChainRequest("eth_getBlockByNumber",
|
||||||
|
new ListParams("safe", false))
|
||||||
|
)
|
||||||
|
|
||||||
|
then:
|
||||||
|
act != null
|
||||||
|
with(act.block()) {
|
||||||
|
it.result.length > 0
|
||||||
|
with(Global.objectMapper.readValue(it.result, BlockJson)) {
|
||||||
|
number == 74735
|
||||||
|
}
|
||||||
|
it.finalization == new FinalizationData(74735, FinalizationType.SAFE_BLOCK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def "getBlockByNumber skips requests with tx bodies"() {
|
def "getBlockByNumber skips requests with tx bodies"() {
|
||||||
setup:
|
setup:
|
||||||
def head = Mock(Head)
|
def head = Mock(Head)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class GrpcHeadSpec extends Specification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
def convert = { BlockchainOuterClass.ChainHead head ->
|
def convert = { BlockchainOuterClass.ChainHead head ->
|
||||||
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of())
|
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of(), List.of())
|
||||||
}
|
}
|
||||||
def head = new GrpcHead(
|
def head = new GrpcHead(
|
||||||
"test",
|
"test",
|
||||||
@@ -127,7 +127,7 @@ class GrpcHeadSpec extends Specification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
def convert = { BlockchainOuterClass.ChainHead head ->
|
def convert = { BlockchainOuterClass.ChainHead head ->
|
||||||
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of())
|
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of(), List.of())
|
||||||
}
|
}
|
||||||
def head = new GrpcHead(
|
def head = new GrpcHead(
|
||||||
"test",
|
"test",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass.BlockTag
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass.HeightSelector
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@@ -53,6 +57,94 @@ class SelectorTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("finalData")
|
||||||
|
fun `sort with finalization`(
|
||||||
|
finalizationType: FinalizationType,
|
||||||
|
finalizationProto: BlockchainOuterClass.BlockTag,
|
||||||
|
) {
|
||||||
|
val up1 = mock<Upstream> {
|
||||||
|
on { getFinalizations() } doReturn listOf(FinalizationData(1L, finalizationType))
|
||||||
|
}
|
||||||
|
val up2 = mock<Upstream> {
|
||||||
|
on { getFinalizations() } doReturn listOf(FinalizationData(10L, finalizationType))
|
||||||
|
}
|
||||||
|
val up3 = mock<Upstream> {
|
||||||
|
on { getFinalizations() } doReturn listOf(FinalizationData(100L, finalizationType))
|
||||||
|
}
|
||||||
|
val up4 = mock<Upstream> {
|
||||||
|
on { getFinalizations() } doReturn listOf()
|
||||||
|
}
|
||||||
|
val ups = listOf(up4, up3, up2, up1)
|
||||||
|
val requestSelectors = listOf(
|
||||||
|
BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setHeightSelector(
|
||||||
|
HeightSelector.newBuilder()
|
||||||
|
.setTag(finalizationProto),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val upstreamFilter = Selector.convertToUpstreamFilter(requestSelectors)
|
||||||
|
|
||||||
|
val actual = ups.sortedWith(upstreamFilter.sort.comparator)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(up3, up2, up1, up4),
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sort with latest`() {
|
||||||
|
val mockHead1 = mock<Head> {
|
||||||
|
on { getCurrentHeight() } doReturn 1L
|
||||||
|
}
|
||||||
|
val up1 = mock<Upstream> {
|
||||||
|
on { getHead() } doReturn mockHead1
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockHead2 = mock<Head> {
|
||||||
|
on { getCurrentHeight() } doReturn 2L
|
||||||
|
}
|
||||||
|
val up2 = mock<Upstream> {
|
||||||
|
on { getHead() } doReturn mockHead2
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockHead3 = mock<Head> {
|
||||||
|
on { getCurrentHeight() } doReturn 3L
|
||||||
|
}
|
||||||
|
val up3 = mock<Upstream> {
|
||||||
|
on { getHead() } doReturn mockHead3
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockHead4 = mock<Head> {
|
||||||
|
on { getCurrentHeight() } doReturn null
|
||||||
|
}
|
||||||
|
val up4 = mock<Upstream> {
|
||||||
|
on { getHead() } doReturn mockHead4
|
||||||
|
}
|
||||||
|
|
||||||
|
val ups = listOf(up2, up1, up4, up3)
|
||||||
|
val requestSelectors = listOf(
|
||||||
|
BlockchainOuterClass.Selector.newBuilder()
|
||||||
|
.setHeightSelector(
|
||||||
|
HeightSelector.newBuilder()
|
||||||
|
.setTag(BlockTag.LATEST),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val upstreamFilter = Selector.convertToUpstreamFilter(requestSelectors)
|
||||||
|
|
||||||
|
val actual = ups.sortedWith(upstreamFilter.sort.comparator)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(up3, up2, up1, up4),
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `preserve the same order if no lower bound type`() {
|
fun `preserve the same order if no lower bound type`() {
|
||||||
val up1 = mock<Upstream> {
|
val up1 = mock<Upstream> {
|
||||||
@@ -127,5 +219,12 @@ class SelectorTest {
|
|||||||
of(LowerBoundType.BLOCK, BlockchainOuterClass.LowerBoundType.LOWER_BOUND_BLOCK),
|
of(LowerBoundType.BLOCK, BlockchainOuterClass.LowerBoundType.LOWER_BOUND_BLOCK),
|
||||||
of(LowerBoundType.SLOT, BlockchainOuterClass.LowerBoundType.LOWER_BOUND_SLOT),
|
of(LowerBoundType.SLOT, BlockchainOuterClass.LowerBoundType.LOWER_BOUND_SLOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun finalData(): List<Arguments> =
|
||||||
|
listOf(
|
||||||
|
of(FinalizationType.SAFE_BLOCK, BlockTag.SAFE),
|
||||||
|
of(FinalizationType.FINALIZED_BLOCK, BlockTag.FINALIZED),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Global
|
||||||
|
import io.emeraldpay.dshackle.reader.ChainReader
|
||||||
|
import io.emeraldpay.dshackle.upstream.ChainRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.ChainResponse
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class EthereumFinalizationDetectorTest {
|
||||||
|
|
||||||
|
private lateinit var upstream: Upstream
|
||||||
|
private lateinit var chainReader: ChainReader
|
||||||
|
private lateinit var detector: EthereumFinalizationDetector
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
upstream = mock()
|
||||||
|
chainReader = mock()
|
||||||
|
`when`(upstream.getIngressReader()).thenReturn(chainReader)
|
||||||
|
detector = EthereumFinalizationDetector()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDetectFinalization() {
|
||||||
|
`when`(chainReader.read(ChainRequest("eth_getBlockByNumber", ListParams("safe", false), 1)))
|
||||||
|
.thenReturn(
|
||||||
|
Mono.just(
|
||||||
|
ChainResponse(
|
||||||
|
Global.objectMapper.writeValueAsString(
|
||||||
|
BlockJson<TransactionRefJson>().apply {
|
||||||
|
number = 1
|
||||||
|
timestamp = Instant.now()
|
||||||
|
},
|
||||||
|
).toByteArray(),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
`when`(chainReader.read(ChainRequest("eth_getBlockByNumber", ListParams("finalized", false), 2)))
|
||||||
|
.thenReturn(
|
||||||
|
Mono.just(
|
||||||
|
ChainResponse(
|
||||||
|
Global.objectMapper.writeValueAsString(
|
||||||
|
BlockJson<TransactionRefJson>().apply {
|
||||||
|
number = 2
|
||||||
|
timestamp = Instant.now()
|
||||||
|
},
|
||||||
|
).toByteArray(),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val flux = detector.detectFinalization(upstream, Duration.ofMillis(200))
|
||||||
|
flux.take(2).collectList().block()
|
||||||
|
val result = detector.getFinalizations().toList()
|
||||||
|
Assertions.assertEquals(2, result.size)
|
||||||
|
org.assertj.core.api.Assertions.assertThat(result)
|
||||||
|
.contains(FinalizationData(2L, FinalizationType.FINALIZED_BLOCK))
|
||||||
|
.contains(FinalizationData(1L, FinalizationType.SAFE_BLOCK))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream.grpc
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.Chain
|
||||||
|
import io.emeraldpay.dshackle.config.ChainsConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.test.MockGrpcServerKt
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
|
||||||
|
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
|
||||||
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||||
|
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import reactor.core.publisher.Sinks
|
||||||
|
import reactor.core.scheduler.Schedulers
|
||||||
|
|
||||||
|
class GenericGrpcUpstreamTest {
|
||||||
|
private val parentId = "testParent"
|
||||||
|
private val hash: Byte = 0x01
|
||||||
|
private val role = UpstreamsConfig.UpstreamRole.PRIMARY
|
||||||
|
private val headSink = Sinks.many().multicast().directBestEffort<BlockchainOuterClass.ChainHead>()
|
||||||
|
private val remote =
|
||||||
|
MockGrpcServerKt().clientForServer(
|
||||||
|
object : BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
override fun subscribeHead(
|
||||||
|
request: Common.Chain,
|
||||||
|
responseObserver: StreamObserver<BlockchainOuterClass.ChainHead>,
|
||||||
|
) {
|
||||||
|
Thread {
|
||||||
|
headSink.asFlux().subscribe { data ->
|
||||||
|
responseObserver.onNext(data)
|
||||||
|
Thread.sleep(500)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
private val client = Mockito.mock(JsonRpcGrpcClient::class.java)
|
||||||
|
private val nodeRating = 5
|
||||||
|
private val overrideLabels = Mockito.mock(UpstreamsConfig.Labels::class.java)
|
||||||
|
private val headScheduler = Schedulers.single()
|
||||||
|
|
||||||
|
private fun getUpstream(): GrpcUpstream {
|
||||||
|
return GenericGrpcUpstream(
|
||||||
|
parentId,
|
||||||
|
hash,
|
||||||
|
role,
|
||||||
|
Chain.LINEA__MAINNET,
|
||||||
|
remote,
|
||||||
|
client,
|
||||||
|
nodeRating,
|
||||||
|
overrideLabels,
|
||||||
|
ChainsConfig.ChainConfig.default(),
|
||||||
|
headScheduler,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun start() {
|
||||||
|
val up = getUpstream()
|
||||||
|
up.getHead().start()
|
||||||
|
up.start()
|
||||||
|
headSink.emitNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setChain(Common.ChainRef.CHAIN_LINEA__MAINNET)
|
||||||
|
.setHeight(10L)
|
||||||
|
.setWeight(ByteString.EMPTY)
|
||||||
|
.setBlockId("a2622ec25e883dd13c1091c18ba717a1a794713baa77b8e68ec6a993045cb50f")
|
||||||
|
.setTimestamp(0)
|
||||||
|
.addAllFinalizationData(
|
||||||
|
mutableListOf(
|
||||||
|
Common
|
||||||
|
.FinalizationData
|
||||||
|
.newBuilder()
|
||||||
|
.setType(Common.FinalizationType.FINALIZATION_FINALIZED_BLOCK)
|
||||||
|
.setHeight(8L)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addAllLowerBounds(
|
||||||
|
mutableListOf(
|
||||||
|
BlockchainOuterClass.LowerBound
|
||||||
|
.newBuilder()
|
||||||
|
.setLowerBoundType(BlockchainOuterClass.LowerBoundType.LOWER_BOUND_TX)
|
||||||
|
.setLowerBoundTimestamp(0)
|
||||||
|
.setLowerBoundValue(1L).build(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
) { _, _ ->
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Thread.sleep(100)
|
||||||
|
Assertions.assertEquals(1, up.getFinalizations().size)
|
||||||
|
Assertions.assertTrue(
|
||||||
|
up.getFinalizations()
|
||||||
|
.contains(FinalizationData(8L, FinalizationType.FINALIZED_BLOCK)),
|
||||||
|
)
|
||||||
|
Assertions.assertTrue(
|
||||||
|
up.getLowerBounds()
|
||||||
|
.contains(LowerBoundData(1L, 0L, LowerBoundType.TX)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getFinalizations() {
|
||||||
|
val up = getUpstream()
|
||||||
|
val finalizationData1 = FinalizationData(100L, FinalizationType.FINALIZED_BLOCK)
|
||||||
|
|
||||||
|
val finalizationData2 = FinalizationData(200L, FinalizationType.FINALIZED_BLOCK)
|
||||||
|
|
||||||
|
up.addFinalization(finalizationData1, "upstream1")
|
||||||
|
up.addFinalization(finalizationData2, "upstream2")
|
||||||
|
|
||||||
|
val finalizations = up.getFinalizations()
|
||||||
|
Assertions.assertEquals(1, finalizations.size)
|
||||||
|
Assertions.assertTrue(finalizations.contains(finalizationData2))
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/test/kotlin/test/MockGrpcServerKt.kt
Normal file
40
src/test/kotlin/test/MockGrpcServerKt.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 ETCDEV GmbH
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.grpc.inprocess.InProcessChannelBuilder
|
||||||
|
import io.grpc.inprocess.InProcessServerBuilder
|
||||||
|
import io.grpc.testing.GrpcCleanupRule
|
||||||
|
import org.junit.Rule
|
||||||
|
|
||||||
|
class MockGrpcServerKt {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val grpcCleanup = GrpcCleanupRule()
|
||||||
|
|
||||||
|
fun clientForServer(impl: BlockchainGrpc.BlockchainImplBase): ReactorBlockchainGrpc.ReactorBlockchainStub {
|
||||||
|
val serverName = InProcessServerBuilder.generateName()
|
||||||
|
grpcCleanup.register(
|
||||||
|
InProcessServerBuilder
|
||||||
|
.forName(serverName).directExecutor().addService(impl).build().start(),
|
||||||
|
)
|
||||||
|
val channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build())
|
||||||
|
return ReactorBlockchainGrpc.newReactorStub(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user