Auto detect labels (#245)

This commit is contained in:
KirillPamPam
2023-07-07 19:22:05 +04:00
committed by GitHub
parent 634f704c2e
commit c29b5037a1
6 changed files with 148 additions and 11 deletions

View File

@@ -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<DefaultOptions> = ArrayList<DefaultOptions>()
@@ -188,7 +189,7 @@ open class UpstreamsConfig {
}
// TODO make it unmodifiable after initial load
class Labels : HashMap<String, String>() {
class Labels : ConcurrentHashMap<String, String>() {
companion object {
@JvmStatic

View File

@@ -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 {

View File

@@ -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<String> {
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 {

View File

@@ -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 {

View File

@@ -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<Pair<String, String>> {
return Flux.merge(
detectNodeType(),
detectArchiveNode()
)
}
private fun detectNodeType(): Mono<Pair<String, String>?> {
return reader
.read(JsonRpcRequest("web3_clientVersion", listOf()))
.flatMap(JsonRpcResponse::requireResult)
.mapNotNull {
val node = objectMapper.readValue<JsonNode>(it)
if (node.isTextual) {
nodeType(node.textValue())?.run {
"client_type" to this
}
} else {
null
}
}
.onErrorResume { Mono.empty() }
}
private fun detectArchiveNode(): Mono<Pair<String, String>> {
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
}
}
}

View File

@@ -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<String, String>("client_type", clientType),
new Pair<String, String>("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))
}
}