diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index b4b9a62b..721d9f53 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -23,6 +23,7 @@ import java.net.URI import java.time.Duration import java.util.Arrays import java.util.Locale +import java.util.concurrent.ConcurrentHashMap open class UpstreamsConfig { var defaultOptions: MutableList = ArrayList() @@ -188,7 +189,7 @@ open class UpstreamsConfig { } // TODO make it unmodifiable after initial load - class Labels : HashMap() { + class Labels : ConcurrentHashMap() { companion object { @JvmStatic diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt index 4cec8fbc..68f5299b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt @@ -34,7 +34,7 @@ abstract class DefaultUpstream( private val options: UpstreamsConfig.Options, private val role: UpstreamsConfig.UpstreamRole, private val targets: CallMethods?, - node: QuorumForLabels.QuorumItem?, + private val node: QuorumForLabels.QuorumItem?, private val chainConfig: ChainsConfig.ChainConfig ) : Upstream { @@ -161,11 +161,9 @@ abstract class DefaultUpstream( override fun nodeId(): Byte = hash - private val quorumByLabel = node?.let { QuorumForLabels(it) } - ?: QuorumForLabels(QuorumForLabels.QuorumItem.empty()) - open fun getQuorumByLabel(): QuorumForLabels { - return quorumByLabel + return node?.let { QuorumForLabels(it) } + ?: QuorumForLabels(QuorumForLabels.QuorumItem.empty()) } override fun getId(): String { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt index 54fd2aed..4b330a86 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt @@ -92,7 +92,8 @@ class DefaultEthereumMethods( "eth_getStorageAt", "eth_getCode", "eth_getUncleByBlockHashAndIndex", - "eth_getLogs" + "eth_getLogs", + "eth_maxPriorityFeePerGas" ) private val specialMethods = listOf( @@ -224,10 +225,12 @@ class DefaultEthereumMethods( } private fun chainUnsupportedMethods(chain: Chain): Set { - if (chain == Chain.OPTIMISM__MAINNET) { - return setOf("eth_getAccounts") + return when (chain) { + Chain.OPTIMISM__MAINNET -> setOf("eth_getAccounts") + Chain.ZKSYNC__MAINNET, Chain.ZKSYNC__TESTNET, Chain.POLYGON_ZKEVM__TESTNET, Chain.POLYGON_ZKEVM__MAINNET -> + setOf("eth_maxPriorityFeePerGas") + else -> emptySet() } - return emptySet() } override fun isCallable(method: String): Boolean { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt index 3a957919..2c3365ae 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt @@ -29,6 +29,7 @@ import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.ethereum.connectors.ConnectorFactory import io.emeraldpay.dshackle.upstream.ethereum.connectors.EthereumConnector +import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumLabelsDetector import org.springframework.context.Lifecycle import reactor.core.Disposable @@ -39,13 +40,14 @@ open class EthereumLikeRpcUpstream( options: UpstreamsConfig.Options, role: UpstreamsConfig.UpstreamRole, targets: CallMethods?, - node: QuorumForLabels.QuorumItem?, + private val node: QuorumForLabels.QuorumItem?, connectorFactory: ConnectorFactory, chainConfig: ChainsConfig.ChainConfig, skipEnhance: Boolean ) : EthereumLikeUpstream(id, hash, options, role, targets, node, chainConfig), Lifecycle, Upstream, CachesEnabled { private val validator: EthereumUpstreamValidator = EthereumUpstreamValidator(this, getOptions()) private val connector: EthereumConnector = connectorFactory.create(this, validator, chain, skipEnhance) + private val labelsDetector = EthereumLabelsDetector(this.getIngressReader()) private var validatorSubscription: Disposable? = null @@ -67,6 +69,13 @@ open class EthereumLikeRpcUpstream( validatorSubscription = validator.start() .subscribe(this::setStatus) } + labelsDetector.detectLabels() + .toStream() + .forEach { + node?.labels?.let { labels -> + labels[it.first] = it.second + } + } } override fun getIngressSubscription(): EthereumIngressSubscription { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/subscribe/EthereumLabelsDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/subscribe/EthereumLabelsDetector.kt new file mode 100644 index 00000000..5ae182c1 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/subscribe/EthereumLabelsDetector.kt @@ -0,0 +1,61 @@ +package io.emeraldpay.dshackle.upstream.ethereum.subscribe + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.readValue +import io.emeraldpay.dshackle.Global.Companion.objectMapper +import io.emeraldpay.dshackle.reader.JsonRpcReader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +class EthereumLabelsDetector( + private val reader: JsonRpcReader +) { + + fun detectLabels(): Flux> { + return Flux.merge( + detectNodeType(), + detectArchiveNode() + ) + } + + private fun detectNodeType(): Mono?> { + return reader + .read(JsonRpcRequest("web3_clientVersion", listOf())) + .flatMap(JsonRpcResponse::requireResult) + .mapNotNull { + val node = objectMapper.readValue(it) + if (node.isTextual) { + nodeType(node.textValue())?.run { + "client_type" to this + } + } else { + null + } + } + .onErrorResume { Mono.empty() } + } + + private fun detectArchiveNode(): Mono> { + return reader + .read(JsonRpcRequest("eth_getBalance", listOf("0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0", "0x1"))) + .flatMap(JsonRpcResponse::requireResult) + .map { "archive" to "true" } + .onErrorResume { Mono.empty() } + } + + private fun nodeType(nodeType: String): String? { + return if (nodeType.contains("erigon", true)) { + "erigon" + } else if (nodeType.contains("geth", true)) { + "geth" + } else if (nodeType.contains("bor", true)) { + "bor" + } else if (nodeType.contains("nethermind", true)) { + "nethermind" + } else { + null + } + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumLabelsDetectorSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumLabelsDetectorSpec.groovy new file mode 100644 index 00000000..77d5d597 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumLabelsDetectorSpec.groovy @@ -0,0 +1,65 @@ +package io.emeraldpay.dshackle.upstream.ethereum + +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.test.ApiReaderMock +import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.DefaultUpstream +import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumLabelsDetector +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import kotlin.Pair +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration + +class EthereumLabelsDetectorSpec extends Specification { + + def "Detect labels"() { + setup: + def up = TestingCommons.upstream( + new ApiReaderMock().tap { + answer("web3_clientVersion", [], response) + answer("eth_getBalance", ["0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0", "0x1"], "") + } + ) + def detector = new EthereumLabelsDetector(up.getIngressReader()) + + when: + def act = detector.detectLabels() + then: + StepVerifier.create(act) + .expectNext( + new Pair("client_type", clientType), + new Pair("archive", "true") + ) + .expectComplete() + .verify(Duration.ofSeconds(1)) + where: + response | clientType + "Nethermind/v1.19.3+e8ac1da4/linux-x64/dotnet7.0.8" | "nethermind" + "Geth/v1.12.0-stable-e501b3b0/linux-amd64/go1.20.3" | "geth" + "Erigon/v1.12.0-stable-e501b3b0/linux-amd64/go1.20.3" | "erigon" + "Bor/v0.4.0/linux-amd64/go1.19.10" | "bor" + } + + def "No any label"() { + setup: + def up = Mock(DefaultUpstream) { + 1 * getIngressReader() >> Mock(Reader) { + 1 * read(new JsonRpcRequest("web3_clientVersion", [])) >> + Mono.just(new JsonRpcResponse('no/v1.19.3+e8ac1da4/linux-x64/dotnet7.0.8'.getBytes(), null)) + 1 * read(new JsonRpcRequest("eth_getBalance", ["0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0", "0x1"])) >> + Mono.error(new RuntimeException()) + } + } + def detector = new EthereumLabelsDetector(up.getIngressReader()) + when: + def act = detector.detectLabels() + then: + StepVerifier.create(act) + .expectComplete() + .verify(Duration.ofSeconds(1)) + } +}