From f3ecbdaaa7262a09edf1c2f1c2d09bf8b7c4141c Mon Sep 17 00:00:00 2001 From: KirillPamPam Date: Fri, 23 Jan 2026 14:33:45 +0400 Subject: [PATCH] Lower bounds fixes (#777) --- .../dshackle/config/UpstreamsConfig.kt | 12 ++ .../dshackle/config/UpstreamsConfigReader.kt | 37 ++++ .../dshackle/upstream/DefaultUpstream.kt | 4 + .../dshackle/upstream/Multistream.kt | 4 + .../emeraldpay/dshackle/upstream/Upstream.kt | 1 + .../EthereumStateLowerBoundErrorHandler.kt | 4 +- .../EthereumLowerBoundBlockDetector.kt | 1 + .../upstream/generic/GenericUpstream.kt | 6 + .../upstream/lowerbound/LowerBoundData.kt | 18 +- .../upstream/lowerbound/LowerBoundDetector.kt | 22 ++- .../upstream/lowerbound/LowerBoundService.kt | 6 +- .../lowerbound/ManualLowerBoundService.kt | 87 +++++++++ .../dshackle/test/GenericUpstreamMock.groovy | 1 + .../dshackle/upstream/FilteredApisSpec.groovy | 1 + .../config/UpstreamsConfigReaderTest.kt | 58 ++++++ ...EthereumStateLowerBoundErrorHandlerTest.kt | 14 ++ .../KadenaLowerBoundStateDetectorTest.kt | 3 +- .../BaseManualLowerBoundServiceTest.kt | 176 ++++++++++++++++++ .../lowerbound/LowerBoundDetectorTest.kt | 95 ++++++++++ .../solana/SolanaLowerBoundServiceTest.kt | 66 +++++++ .../StarknetLowerBoundStateDetectorTest.kt | 3 +- 21 files changed, 608 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/ManualLowerBoundService.kt create mode 100644 src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/BaseManualLowerBoundServiceTest.kt create mode 100644 src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetectorTest.kt diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index f1b51ec8..9aff5935 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory.ConnectorMode +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import io.emeraldpay.dshackle.upstream.lowerbound.ManualLowerBoundType import java.net.URI import java.util.Arrays import java.util.Locale @@ -42,6 +44,7 @@ data class UpstreamsConfig( var methodGroups: MethodGroups? = null, var role: UpstreamRole = UpstreamRole.PRIMARY, var customHeaders: Map = emptyMap(), + var additionalSettings: AdditionalSettings? = null, ) { @Suppress("UNCHECKED_CAST") @@ -188,6 +191,15 @@ data class UpstreamsConfig( } } + data class AdditionalSettings( + val manualLowerBounds: Map, + ) + + data class ManualBoundSetting( + val type: ManualLowerBoundType, + val value: Long, + ) + data class Methods( val enabled: Set, val disabled: Set, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt index 7e16342a..dfcad899 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt @@ -17,12 +17,16 @@ package io.emeraldpay.dshackle.config import io.emeraldpay.dshackle.FileResolver +import io.emeraldpay.dshackle.config.UpstreamsConfig.ManualBoundSetting import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.foundation.ChainOptionsReader import io.emeraldpay.dshackle.foundation.YamlConfigReader +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import io.emeraldpay.dshackle.upstream.lowerbound.ManualLowerBoundType import org.apache.commons.lang3.StringUtils import org.slf4j.LoggerFactory import org.yaml.snakeyaml.nodes.MappingNode +import org.yaml.snakeyaml.nodes.Tag import java.io.InputStream import java.net.URI import java.util.Locale @@ -287,6 +291,7 @@ class UpstreamsConfigReader( upstream.options = optionsReader.read(upNode) upstream.methods = tryReadMethods(upNode) upstream.methodGroups = tryReadMethodGroups(upNode) + upstream.additionalSettings = readAdditionalSettings(upNode) getValueAsBool(upNode, "enabled")?.let { upstream.isEnabled = it } @@ -311,6 +316,38 @@ class UpstreamsConfigReader( } } + private fun readAdditionalSettings(upNode: MappingNode): UpstreamsConfig.AdditionalSettings? { + return getMapping(upNode, "additional-settings") + ?.let { settings -> + val manualLowerBounds = getMapping(settings, "manual-lower-bounds") + ?.let { bounds -> + bounds.value.asSequence() + .filter { + StringUtils.isNotBlank(it.keyNode.valueAsString()) && it.valueNode.tag == Tag.MAP + } + .map { + val setting = readManualBoundSetting(it.valueNode as MappingNode) + + if (setting != null) { + LowerBoundType.byName(it.keyNode.valueAsString()!!) to setting + } else { + null + } + } + .filterNotNull() + .associate { it.first to it.second } + } ?: emptyMap() + UpstreamsConfig.AdditionalSettings(manualLowerBounds) + } + } + + private fun readManualBoundSetting(node: MappingNode): ManualBoundSetting? { + val type = getValueAsString(node, "type") + ?.let { ManualLowerBoundType.byName(it) } ?: return null + val value = getValueAsLong(node, "value") ?: return null + return ManualBoundSetting(type, value) + } + private fun readUpstreamGrpc( upNode: MappingNode, ) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt index 3ea8649b..5fd5d758 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt @@ -172,6 +172,10 @@ abstract class DefaultUpstream( return 0 } + override fun getAdditionalSettings(): UpstreamsConfig.AdditionalSettings? { + return null + } + protected fun sendUpstreamStateEvent(eventType: UpstreamChangeEvent.ChangeType) { stateEventStream.emitNext( UpstreamChangeEvent(chain, this, eventType), diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt index 6f04efcc..c4680e4f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt @@ -261,6 +261,10 @@ abstract class Multistream( return 0 } + override fun getAdditionalSettings(): UpstreamsConfig.AdditionalSettings? { + return null + } + override fun getStatus(): UpstreamAvailability { return state.getStatus() } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt index 2a091cb6..b1fdac7e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt @@ -56,6 +56,7 @@ interface Upstream : Lifecycle { fun getUpstreamSettingsData(): UpstreamSettingsData? fun updateLowerBound(lowerBound: Long, type: LowerBoundType) fun predictLowerBound(type: LowerBoundType, timeOffsetSeconds: Long): Long + fun getAdditionalSettings(): UpstreamsConfig.AdditionalSettings? fun getChain(): Chain diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandler.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandler.kt index 1916b788..ea0565e2 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandler.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandler.kt @@ -21,7 +21,9 @@ object EthereumStateLowerBoundErrorHandler : EthereumLowerBoundErrorHandler() { private val applicableMethods = firstTagIndexMethods + secondTagIndexMethods override fun canHandle(request: ChainRequest, errorMessage: String?): Boolean { - return stateErrors.any { errorMessage?.contains(it) ?: false } && applicableMethods.contains(request.method) + return !(errorMessage?.contains("execution reverted") ?: false) && + stateErrors.any { errorMessage?.contains(it) ?: false } && + applicableMethods.contains(request.method) } override fun tagIndex(method: String): Int { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundBlockDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundBlockDetector.kt index dde67f04..c67d6ea4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundBlockDetector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundBlockDetector.kt @@ -29,6 +29,7 @@ class EthereumLowerBoundBlockDetector( "Unexpected error", // hyperliquid "invalid block height", // hyperliquid "pruned history unavailable", // xlayer + "no transactions snapshot file for", ) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt index ecd3e63a..c539d261 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt @@ -64,6 +64,7 @@ open class GenericUpstream( lowerBoundServiceBuilder: LowerBoundServiceBuilder, finalizationDetectorBuilder: FinalizationDetectorBuilder, versionRules: Supplier, + private val additionalSettings: UpstreamsConfig.AdditionalSettings?, ) : DefaultUpstream(id, hash, null, UpstreamAvailability.OK, options, role, targets, node, chainConfig, chain), Lifecycle { constructor( @@ -96,6 +97,7 @@ open class GenericUpstream( lowerBoundServiceBuilder, finalizationDetectorBuilder, versionRules, + config.additionalSettings, ) { rpcMethodsDetector = upstreamRpcMethodsDetectorBuilder(this, config) detectRpcMethods(config, buildMethods) @@ -443,6 +445,10 @@ open class GenericUpstream( return lowerBoundService.predictLowerBound(type, timeOffsetSeconds) } + override fun getAdditionalSettings(): UpstreamsConfig.AdditionalSettings? { + return additionalSettings + } + fun isValid(): Boolean = isUpstreamValid.get() companion object { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundData.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundData.kt index 20ccc7a0..175686f4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundData.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundData.kt @@ -20,7 +20,23 @@ data class LowerBoundData( } enum class LowerBoundType { - UNKNOWN, STATE, SLOT, BLOCK, TX, LOGS, TRACE, PROOF, BLOB, EPOCH, RECEIPTS + UNKNOWN, STATE, SLOT, BLOCK, TX, LOGS, TRACE, PROOF, BLOB, EPOCH, RECEIPTS; + + companion object { + fun byName(name: String): LowerBoundType { + return entries.firstOrNull { it.name.equals(name, ignoreCase = true) } ?: UNKNOWN + } + } +} + +enum class ManualLowerBoundType { + HEAD, FIXED; + + companion object { + fun byName(name: String): ManualLowerBoundType? { + return entries.firstOrNull { it.name.equals(name, ignoreCase = true) } + } + } } fun BlockchainOuterClass.LowerBoundType.fromProtoType(): LowerBoundType { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetector.kt index 857ee960..2dbd5002 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetector.kt @@ -18,7 +18,7 @@ abstract class LowerBoundDetector( protected val lowerBounds = LowerBounds(chain) private val lowerBoundSink = Sinks.many().multicast().directBestEffort() - fun detectLowerBound(): Flux { + fun detectLowerBound(manualBoundsService: ManualLowerBoundService): Flux { val notProcessing = AtomicBoolean(true) return Flux.merge( @@ -29,11 +29,21 @@ abstract class LowerBoundDetector( ) .filter { notProcessing.get() } .flatMap { - notProcessing.set(false) - internalDetectLowerBound() - .onErrorResume { Mono.just(LowerBoundData.default()) } - .switchIfEmpty(Flux.just(LowerBoundData.default())) - .doFinally { notProcessing.set(true) } + if (types() == manualBoundsService.manualBoundTypes()) { + Flux.fromIterable(types()) + .mapNotNull { manualBoundsService.manualLowerBound(it) } + } else { + notProcessing.set(false) + Flux.merge( + internalDetectLowerBound() + .filter { !manualBoundsService.hasManualBound(it.type) } + .onErrorResume { Mono.just(LowerBoundData.default()) } + .switchIfEmpty(Flux.just(LowerBoundData.default())) + .doFinally { notProcessing.set(true) }, + Flux.fromIterable(types()) + .mapNotNull { manualBoundsService.manualLowerBound(it) }, + ) + } }, ) .filter { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundService.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundService.kt index c0693aa8..c82ef43b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundService.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundService.kt @@ -16,10 +16,14 @@ abstract class LowerBoundService( private val lowerBounds = ConcurrentHashMap() private val detectors: List by lazy { detectors() } + private val manualBoundsService = BaseManualLowerBoundService( + upstream, + upstream.getAdditionalSettings()?.manualLowerBounds ?: emptyMap(), + ) fun detectLowerBounds(): Flux { return Flux.merge( - detectors.map { it.detectLowerBound() }, + detectors.map { it.detectLowerBound(manualBoundsService) }, ) .doOnNext { log.info("Lower bound of type ${it.type} is ${it.lowerBound} for upstream ${upstream.getId()} of chain $chain") diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/ManualLowerBoundService.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/ManualLowerBoundService.kt new file mode 100644 index 00000000..2bbadcdf --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/ManualLowerBoundService.kt @@ -0,0 +1,87 @@ +package io.emeraldpay.dshackle.upstream.lowerbound + +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.Upstream + +interface ManualLowerBoundService { + fun manualLowerBound(type: LowerBoundType): LowerBoundData? + fun hasManualBound(type: LowerBoundType): Boolean + fun manualBoundTypes(): Set +} + +class NoopManualLowerBoundService : ManualLowerBoundService { + override fun manualLowerBound(type: LowerBoundType): LowerBoundData? { + return null + } + + override fun hasManualBound(type: LowerBoundType): Boolean { + return false + } + + override fun manualBoundTypes(): Set { + return emptySet() + } +} + +class BaseManualLowerBoundService( + private val upstream: Upstream, + private val manualLowerBoundSettings: Map, +) : ManualLowerBoundService { + private val providers = manualLowerBoundSettings.mapValues { provider(it.value) } + + override fun manualLowerBound(type: LowerBoundType): LowerBoundData? { + val provider = providers[type] ?: return null + if (!provider.canHandle()) { + return null + } + return LowerBoundData(provider.getManualLowerBound(), type) + } + + override fun hasManualBound(type: LowerBoundType): Boolean { + return manualLowerBoundSettings.contains(type) + } + + override fun manualBoundTypes(): Set { + return manualLowerBoundSettings.keys + } + + private fun provider(setting: UpstreamsConfig.ManualBoundSetting): ManualLowerBoundProvider { + return when (setting.type) { + ManualLowerBoundType.HEAD -> HeadManualLowerBoundProvider(upstream, setting) + ManualLowerBoundType.FIXED -> FixedManualLowerBoundProvider(setting) + } + } +} + +interface ManualLowerBoundProvider { + fun getManualLowerBound(): Long + fun canHandle(): Boolean +} + +class HeadManualLowerBoundProvider( + private val upstream: Upstream, + private val setting: UpstreamsConfig.ManualBoundSetting, +) : ManualLowerBoundProvider { + override fun getManualLowerBound(): Long { + return upstream.getHead().getCurrentHeight()!! + setting.value + } + + override fun canHandle(): Boolean { + return setting.type == ManualLowerBoundType.HEAD && + setting.value < 0 && + upstream.getHead().getCurrentHeight() != null && + upstream.getHead().getCurrentHeight()!! + setting.value >= 0 + } +} + +class FixedManualLowerBoundProvider( + private val setting: UpstreamsConfig.ManualBoundSetting, +) : ManualLowerBoundProvider { + override fun getManualLowerBound(): Long { + return setting.value + } + + override fun canHandle(): Boolean { + return setting.type == ManualLowerBoundType.FIXED && setting.value > 0 + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/GenericUpstreamMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/GenericUpstreamMock.groovy index b22bff24..7fd0eb6b 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/GenericUpstreamMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/GenericUpstreamMock.groovy @@ -79,6 +79,7 @@ class GenericUpstreamMock extends GenericUpstream { io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&lowerBoundService, io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&finalizationDetectorBuilder, [get: { null }] as java.util.function.Supplier, + null, ) this.ethereumHeadMock = this.getHead() as EthereumHeadMock setLag(0) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy index 417a3227..0a9bdda7 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy @@ -80,6 +80,7 @@ class FilteredApisSpec extends Specification { cs.&lowerBoundService, cs.&finalizationDetectorBuilder, [get: { null }] as java.util.function.Supplier, + null, ) } def matcher = new Selector.LabelMatcher("test", ["foo"]) diff --git a/src/test/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReaderTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReaderTest.kt index ccd273ff..b28d89f6 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReaderTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReaderTest.kt @@ -2,6 +2,8 @@ package io.emeraldpay.dshackle.config import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.foundation.ChainOptionsReader +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import io.emeraldpay.dshackle.upstream.lowerbound.ManualLowerBoundType import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue @@ -10,6 +12,62 @@ import java.io.File class UpstreamsConfigReaderTest { + @Test + fun `should parse additional settings with lower bounds`() { + val yaml = """ + version: v1 + upstreams: + - id: test-upstream + chain: ethereum + additional-settings: + manual-lower-bounds: + state: + type: head + value: -5000000 + block: + type: fixed + value: 1 + tx: + type: fixed + value: 123 + receipts: + type: fixed + value: 15661 + slot: + type: head + value: -34566 + connection: + ethereum: + rpc: + url: "http://localhost:8545" + """.trimIndent() + + val reader = UpstreamsConfigReader( + FileResolver(File(".")), + ChainOptionsReader(), + ) + + val config = reader.readInternal(yaml.byteInputStream()) + + assertNotNull(config) + assertEquals(1, config.upstreams.size) + + val upstream = config.upstreams[0] + assertEquals("test-upstream", upstream.id) + assertEquals( + UpstreamsConfig.AdditionalSettings( + mapOf( + LowerBoundType.SLOT to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, -34566), + LowerBoundType.RECEIPTS to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 15661), + LowerBoundType.TX to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 123), + LowerBoundType.BLOCK to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 1), + LowerBoundType.STATE to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, -5000000), + ), + ), + upstream.additionalSettings, + ) + } + @Test fun `should parse customHeaders from YAML`() { val yaml = """ diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandlerTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandlerTest.kt index 12ea47f9..ab1908e4 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandlerTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/error/EthereumStateLowerBoundErrorHandlerTest.kt @@ -42,6 +42,20 @@ class EthereumStateLowerBoundErrorHandlerTest { verify(upstream, never()).updateLowerBound(anyLong(), any()) } + @Test + fun `no update lower bound if execution reverted`() { + val upstream = mock() + val request = ChainRequest( + "eth_call", + ListParams("0x343", "0xCB5A0A8"), + ) + val handler = EthereumStateLowerBoundErrorHandler + + handler.handle(upstream, request, "execution reverted: Fallback not supported") + + verify(upstream, never()).updateLowerBound(anyLong(), any()) + } + @Test fun `no update lower bound if there is a non-state method`() { val upstream = mock() diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt index 6217fbc8..5ba02c46 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt @@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.upstream.kadena import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import io.emeraldpay.dshackle.upstream.lowerbound.NoopManualLowerBoundService import org.junit.jupiter.api.Test import reactor.test.StepVerifier import java.time.Duration @@ -13,7 +14,7 @@ class KadenaLowerBoundStateDetectorTest { fun `kadena lower block is 1`() { val detector = KadenaLowerBoundStateDetector(Chain.KADENA__MAINNET) - StepVerifier.withVirtualTime { detector.detectLowerBound() } + StepVerifier.withVirtualTime { detector.detectLowerBound(NoopManualLowerBoundService()) } .expectSubscription() .expectNoEvent(Duration.ofSeconds(15)) .expectNext(LowerBoundData(1, LowerBoundType.STATE)) diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/BaseManualLowerBoundServiceTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/BaseManualLowerBoundServiceTest.kt new file mode 100644 index 00000000..ef4fc11b --- /dev/null +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/BaseManualLowerBoundServiceTest.kt @@ -0,0 +1,176 @@ +package io.emeraldpay.dshackle.upstream.lowerbound + +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Upstream +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +class BaseManualLowerBoundServiceTest { + + @Test + fun `no manual settings then return nothing`() { + val upstream = mock() + val service = BaseManualLowerBoundService(upstream, emptyMap()) + + assertThat(service.manualBoundTypes()).isEmpty() + LowerBoundType.entries + .forEach { + assertThat(service.manualLowerBound(it)).isNull() + assertThat(service.hasManualBound(it)).isFalse + } + } + + @ParameterizedTest + @MethodSource("headProviderData") + fun `test head provider - canHandle`( + upstream: Upstream, + setting: UpstreamsConfig.ManualBoundSetting, + expected: Boolean, + ) { + val provider = HeadManualLowerBoundProvider(upstream, setting) + + assertThat(provider.canHandle()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("fixedProviderData") + fun `test fixed provider - canHandle`( + setting: UpstreamsConfig.ManualBoundSetting, + expected: Boolean, + ) { + val provider = FixedManualLowerBoundProvider(setting) + + assertThat(provider.canHandle()).isEqualTo(expected) + } + + @Test + fun `get value from head provider`() { + val upstream = mock { + val head = mock { + on { getCurrentHeight() } doReturn 100 + } + on { getHead() } doReturn head + } + val provider = HeadManualLowerBoundProvider(upstream, UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, -45)) + + assertThat(provider.getManualLowerBound()).isEqualTo(55) + } + + @Test + fun `get value from fixed provider`() { + val provider = FixedManualLowerBoundProvider(UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 200)) + + assertThat(provider.getManualLowerBound()).isEqualTo(200) + } + + @Test + fun `no supported manual type then null otherwise get a value`() { + val upstream = mock() + val settings = mapOf( + LowerBoundType.STATE to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 100), + ) + val service = BaseManualLowerBoundService(upstream, settings) + + assertThat(service.manualLowerBound(LowerBoundType.STATE)) + .usingRecursiveComparison() + .ignoringFields("timestamp") + .isEqualTo(LowerBoundData(100, LowerBoundType.STATE)) + assertThat(service.manualBoundTypes()).containsExactly(LowerBoundType.STATE) + assertThat(service.hasManualBound(LowerBoundType.STATE)).isTrue() + LowerBoundType.entries + .filter { it != LowerBoundType.STATE } + .forEach { + assertThat(service.manualLowerBound(it)).isNull() + assertThat(service.hasManualBound(it)).isFalse + } + } + + @Test + fun `can not be handled then null`() { + val upstream = mock() + val settings = mapOf( + LowerBoundType.STATE to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, -100), + ) + val service = BaseManualLowerBoundService(upstream, settings) + + assertThat(service.manualLowerBound(LowerBoundType.STATE)).isNull() + assertThat(service.manualBoundTypes()).containsExactly(LowerBoundType.STATE) + assertThat(service.hasManualBound(LowerBoundType.STATE)).isTrue() + LowerBoundType.entries + .filter { it != LowerBoundType.STATE } + .forEach { + assertThat(service.manualLowerBound(it)).isNull() + assertThat(service.hasManualBound(it)).isFalse + } + } + + companion object { + @JvmStatic + fun fixedProviderData(): List = + listOf( + Arguments.of( + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, 1), + false, + ), + Arguments.of( + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, -121), + false, + ), + Arguments.of( + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 55), + true, + ), + ) + + @JvmStatic + fun headProviderData(): List = + listOf( + Arguments.of( + mock(), + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 1), + false, + ), + Arguments.of( + mock(), + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, 1241), + false, + ), + Arguments.of( + mock { + val head = mock { + on { getCurrentHeight() } doReturn null + } + on { getHead() } doReturn head + }, + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, -100), + false, + ), + Arguments.of( + mock { + val head = mock { + on { getCurrentHeight() } doReturn 20 + } + on { getHead() } doReturn head + }, + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, -100), + false, + ), + Arguments.of( + mock { + val head = mock { + on { getCurrentHeight() } doReturn 150 + } + on { getHead() } doReturn head + }, + UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.HEAD, -100), + true, + ), + ) + } +} diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetectorTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetectorTest.kt new file mode 100644 index 00000000..9768f07a --- /dev/null +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/LowerBoundDetectorTest.kt @@ -0,0 +1,95 @@ +package io.emeraldpay.dshackle.upstream.lowerbound + +import io.emeraldpay.dshackle.Chain +import org.junit.jupiter.api.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import reactor.core.publisher.Flux +import reactor.test.StepVerifier +import java.time.Duration + +class LowerBoundDetectorTest { + + @Test + fun `receive only manual bounds`() { + val detector = TestLowerBoundDetector() + val manualBoundService = mock { + on { manualBoundTypes() } doReturn setOf(LowerBoundType.STATE, LowerBoundType.BLOCK) + on { hasManualBound(LowerBoundType.STATE) } doReturn true + on { hasManualBound(LowerBoundType.BLOCK) } doReturn true + on { manualLowerBound(LowerBoundType.STATE) } doReturn LowerBoundData(50L, 1000, LowerBoundType.STATE) + on { manualLowerBound(LowerBoundType.BLOCK) } doReturn LowerBoundData(80L, 1000, LowerBoundType.BLOCK) + } + + StepVerifier.withVirtualTime { detector.detectLowerBound(manualBoundService) } + .expectSubscription() + .expectNoEvent(Duration.ofSeconds(15)) + .expectNextSequence( + setOf( + LowerBoundData(50L, 1000, LowerBoundType.STATE), + LowerBoundData(80L, 1000, LowerBoundType.BLOCK), + ), + ) + .thenCancel() + .verify(Duration.ofSeconds(3)) + } + + @Test + fun `receive one manual and one calculated bounds`() { + val detector = TestLowerBoundDetector() + val manualBoundService = mock { + on { manualBoundTypes() } doReturn setOf(LowerBoundType.STATE) + on { hasManualBound(LowerBoundType.STATE) } doReturn true + on { hasManualBound(LowerBoundType.BLOCK) } doReturn false + on { manualLowerBound(LowerBoundType.STATE) } doReturn LowerBoundData(50L, 1000, LowerBoundType.STATE) + on { manualLowerBound(LowerBoundType.BLOCK) } doReturn null + } + + StepVerifier.withVirtualTime { detector.detectLowerBound(manualBoundService) } + .expectSubscription() + .expectNoEvent(Duration.ofSeconds(15)) + .expectNextSequence( + setOf( + LowerBoundData(5000L, 1000, LowerBoundType.BLOCK), + LowerBoundData(50L, 1000, LowerBoundType.STATE), + ), + ) + .thenCancel() + .verify(Duration.ofSeconds(3)) + } + + @Test + fun `receive only calculated bounds`() { + val detector = TestLowerBoundDetector() + val manualBoundService = BaseManualLowerBoundService(mock(), emptyMap()) + + StepVerifier.withVirtualTime { detector.detectLowerBound(manualBoundService) } + .expectSubscription() + .expectNoEvent(Duration.ofSeconds(15)) + .expectNextSequence( + setOf( + LowerBoundData(1000L, 1000, LowerBoundType.STATE), + LowerBoundData(5000L, 1000, LowerBoundType.BLOCK), + ), + ) + .thenCancel() + .verify(Duration.ofSeconds(3)) + } +} + +private class TestLowerBoundDetector : LowerBoundDetector(Chain.CORE__MAINNET) { + override fun period(): Long { + return 1 + } + + override fun internalDetectLowerBound(): Flux { + return Flux.just( + LowerBoundData(1000L, 1000, LowerBoundType.STATE), + LowerBoundData(5000L, 1000, LowerBoundType.BLOCK), + ) + } + + override fun types(): Set { + return setOf(LowerBoundType.STATE, LowerBoundType.BLOCK) + } +} diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/solana/SolanaLowerBoundServiceTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/solana/SolanaLowerBoundServiceTest.kt index 7d0be676..d0d48809 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/solana/SolanaLowerBoundServiceTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/solana/SolanaLowerBoundServiceTest.kt @@ -2,12 +2,14 @@ package io.emeraldpay.dshackle.upstream.solana import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Global +import io.emeraldpay.dshackle.config.UpstreamsConfig 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.lowerbound.LowerBoundData import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import io.emeraldpay.dshackle.upstream.lowerbound.ManualLowerBoundType import io.emeraldpay.dshackle.upstream.rpcclient.ListParams import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -76,4 +78,68 @@ class SolanaLowerBoundServiceTest { ), ) } + + @Test + fun `get solana lower block and slot with manual bounds`() { + val reader = mock { + on { read(ChainRequest("getFirstAvailableBlock", ListParams())) } doReturn + Mono.just(ChainResponse("25000000".toByteArray(), null)) + on { + read( + ChainRequest( + "getBlock", + ListParams( + 25000000L, + mapOf( + "showRewards" to false, + "transactionDetails" to "none", + "maxSupportedTransactionVersion" to 0, + ), + ), + ), + ) + } doReturn Mono.just( + ChainResponse( + Global.objectMapper.writeValueAsBytes( + mapOf( + "blockHeight" to 21000000, + "blockTime" to 111, + "blockhash" to "22", + "previousBlockhash" to "33", + ), + ), + null, + ), + ) + } + val settings = UpstreamsConfig.AdditionalSettings( + mapOf( + LowerBoundType.SLOT to UpstreamsConfig.ManualBoundSetting(ManualLowerBoundType.FIXED, 54L), + ), + ) + val upstream = mock { + on { getIngressReader() } doReturn reader + on { getChain() } doReturn Chain.UNSPECIFIED + on { getAdditionalSettings() } doReturn settings + } + + val detector = SolanaLowerBoundService(Chain.UNSPECIFIED, upstream) + + StepVerifier.withVirtualTime { detector.detectLowerBounds() } + .expectSubscription() + .expectNoEvent(Duration.ofSeconds(15)) + .expectNextMatches { it.lowerBound == 21000000L && it.type == LowerBoundType.STATE } + .expectNextMatches { it.lowerBound == 54L && it.type == LowerBoundType.SLOT } + .thenCancel() + .verify(Duration.ofSeconds(3)) + + assertThat(detector.getLowerBounds().toList()) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("timestamp") + .hasSameElementsAs( + listOf( + LowerBoundData(21000000L, LowerBoundType.STATE), + LowerBoundData(54L, LowerBoundType.SLOT), + ), + ) + } } diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetLowerBoundStateDetectorTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetLowerBoundStateDetectorTest.kt index 473aefba..79820bed 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetLowerBoundStateDetectorTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetLowerBoundStateDetectorTest.kt @@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.upstream.starknet import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import io.emeraldpay.dshackle.upstream.lowerbound.NoopManualLowerBoundService import org.junit.jupiter.api.Test import reactor.test.StepVerifier import java.time.Duration @@ -13,7 +14,7 @@ class StarknetLowerBoundStateDetectorTest { fun `starknet lower block is 1`() { val detector = StarknetLowerBoundStateDetector(Chain.STARKNET__MAINNET) - StepVerifier.withVirtualTime { detector.detectLowerBound() } + StepVerifier.withVirtualTime { detector.detectLowerBound(NoopManualLowerBoundService()) } .expectSubscription() .expectNoEvent(Duration.ofSeconds(15)) .expectNext(LowerBoundData(1, LowerBoundType.STATE))