diff --git a/docs/04-upstream-config.adoc b/docs/04-upstream-config.adoc index bfcbbf0e..6ce9f4b8 100644 --- a/docs/04-upstream-config.adoc +++ b/docs/04-upstream-config.adoc @@ -98,6 +98,68 @@ Dshackle currently supports - `ws` websocket connection (supposed to be used in addition to `rpc` connection) - `grpc` connects to another Dshackle instance +=== Methods + +.By default an ethereum upstream supports following JSON RPC methods: +- `eth_gasPrice` +- `eth_call` +- `eth_estimateGas` +- `eth_getBlockTransactionCountByHash` +- `eth_getUncleCountByBlockHash` +- `eth_getBlockByHash` +- `eth_getTransactionByHash` +- `eth_getTransactionByBlockHashAndIndex` +- `eth_getStorageAt` +- `eth_getCode` +- `eth_getUncleByBlockHashAndIndex` +- `eth_getTransactionCount` +- `eth_blockNumber` +- `eth_getBalance` +- `eth_sendRawTransaction` +- `eth_getBlockTransactionCountByNumber` +- `eth_getUncleCountByBlockNumber` +- `eth_getBlockByNumber` +- `eth_getTransactionByBlockNumberAndIndex` +- `eth_getTransactionReceipt` +- `eth_getUncleByBlockNumberAndIndex` + + +.Plus following methods are answered directly by Dshackle +- `net_version` +- `net_peerCount` +- `net_listening` +- `web3_clientVersion` +- `eth_protocolVersion` +- `eth_syncing` +- `eth_coinbase` +- `eth_mining` +- `eth_hashrate` +- `eth_accounts` + +It's possible to enable additional methods that are available on upstream, or disable an existing method. For that purpose +there is `methods` configuration: + +[source, yaml] +---- +upstreams: + - id: my-node + chain: ethereum + labels: + archive: true + methods: + enabled: + - name: trace_transaction + disabled: + - name: eth_getBlockByNumber +---- + +Such configuration option allows to execute methods `trace_transaction` and also disables `eth_getBlockByNumber` on that +particular upstream. If a client tries to execute method `trace_transaction` it will be executed on that upstream, or +another upstream will have such method enabled. + +Together with label `archive: true` it's possible to specify during execution that a client wants to execute method only +on an archive node. + === Authentication ==== TLS diff --git a/docs/06-methods.adoc b/docs/06-methods.adoc index caf54f74..b3e49187 100644 --- a/docs/06-methods.adoc +++ b/docs/06-methods.adoc @@ -52,8 +52,8 @@ Where: - `items` as a list of independent requests, which may be executed in different nodes in parallels or in different order, with: * `method` - a JSON RPC standard name, ex: `eth_getBlockByHash` * `payload` - list of parameters for the methods, encoded as JSON string, ex. `["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331", true]` -- `Selector` and `AvailabilityEnum` are described in reference, in short they allow to specify which nodes are allowed -to execute the request. +- `Selector` and `AvailabilityEnum` are described in reference, in short they allow to specify which nodes must be selected + to execute the reques (i.e. "execute only on an archive node") .NativeCallReplyItem [source,proto] diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index cd3801a8..42462938 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -47,6 +47,7 @@ class UpstreamsConfig { } companion object { + @JvmStatic fun getDefaults(): Options { val options = Options() options.minPeers = 1 @@ -69,6 +70,7 @@ class UpstreamsConfig { var isEnabled = true var connection: T? = null val labels = Labels() + var methods: Methods? = null } open class UpstreamConnection @@ -149,4 +151,13 @@ class UpstreamsConfig { } } } + + class Methods( + val enabled: Set, + val disabled: Set + ) + + class Method( + val name: String + ) } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt index 09bcd001..421be80e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt @@ -102,6 +102,7 @@ class UpstreamsConfigReader { internal fun readUpstreamCommon(upNode: MappingNode, upstream: UpstreamsConfig.Upstream<*>) { upstream.id = getValueAsString(upNode, "id") upstream.options = tryReadOptions(upNode) + upstream.methods = tryReadMethods(upNode) } internal fun readUpstreamGrpc(upNode: MappingNode, upstream: UpstreamsConfig.Upstream) { @@ -139,6 +140,29 @@ class UpstreamsConfigReader { } } + internal fun tryReadMethods(upNode: MappingNode): UpstreamsConfig.Methods? { + return getMapping(upNode, "methods")?.let { mnode -> + val enabled = getList(mnode, "enabled")?.value?.map { m -> + getValueAsString(m, "name")?.let { name -> + UpstreamsConfig.Method( + name = name + ) + } + }?.filterNotNull()?.toSet() ?: emptySet() + val disabled = getList(mnode, "disabled")?.value?.map { m -> + getValueAsString(m, "name")?.let { name -> + UpstreamsConfig.Method( + name = name + ) + } + }?.filterNotNull()?.toSet() ?: emptySet() + + UpstreamsConfig.Methods( + enabled, disabled + ) + } + } + internal fun readOptions(values: MappingNode): UpstreamsConfig.Options { val options = UpstreamsConfig.Options() getValueAsInt(values, "min-peers")?.let { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt index ee3f7d1a..1ed42c66 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt @@ -36,7 +36,7 @@ class Describe( upstreams.getAvailable().forEach { chain -> upstreams.getUpstream(chain)?.let { chainUpstreams -> val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll()) - val targets = chainUpstreams.getSupportedTargets() + val targets = chainUpstreams.getMethods().getSupportedMethods() val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder() .setChain(Common.ChainRef.forNumber(chain.id)) .addAllSupportedMethods(targets) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt index 8fc1fe4d..713f4a9b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt @@ -95,11 +95,16 @@ class NativeCall( } fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux> { - val matcher = Selector.convertToMatcher(request.selector) return request.itemsList.toFlux().map { val method = it.method val params = it.payload.toStringUtf8() - val callQuorum = upstream.targets?.getQuorumFor(method) ?: AlwaysQuorum() + + val matcher = Selector.Builder() + .forMethod(method) + .forLabels(Selector.convertToMatcher(request.selector)) + .build() + + val callQuorum = upstream.getMethods().getQuorumFor(method) ?: AlwaysQuorum() callQuorum.init(upstream.getHead()) CallContext(it.id, upstream, matcher, callQuorum, RawCallDetails(method, params)) @@ -154,7 +159,11 @@ class NativeCall( return req as List } - open class CallContext(val id: Int, val upstream: AggregatedUpstream, val matcher: Selector.Matcher, val callQuorum: CallQuorum, val payload: T) { + open class CallContext(val id: Int, + val upstream: AggregatedUpstream, + val matcher: Selector.Matcher, + val callQuorum: CallQuorum, + val payload: T) { fun withPayload(payload: X): CallContext { return CallContext(id, upstream, matcher, callQuorum, payload) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedCallMethods.kt new file mode 100644 index 00000000..e2967f06 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedCallMethods.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2019 ETCDEV GmbH + * + * 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.upstream + +import io.emeraldpay.dshackle.quorum.CallQuorum +import java.util.* +import kotlin.collections.HashSet + +class AggregatedCallMethods( + private val delegates: Collection +): CallMethods { + + private val allMethods: Set + + init { + val buf = HashSet() + delegates.map { it.getSupportedMethods().forEach { m -> buf.add(m) } } + allMethods = Collections.unmodifiableSet(buf) + } + + override fun getQuorumFor(method: String): CallQuorum { + return delegates.find { + it.isAllowed(method) + }?.getQuorumFor(method) ?: throw IllegalStateException("No quorum for $method") + } + + override fun isAllowed(method: String): Boolean { + return delegates.any { it.isAllowed(method) } + } + + override fun getSupportedMethods(): Set { + return allMethods + } + + override fun isHardcoded(method: String): Boolean { + return delegates.any { it.isAllowed(method) && it.isHardcoded(method) } + } + + override fun executeHardcoded(method: String): Any { + return delegates.find { + it.isAllowed(method) && it.isHardcoded(method) + }?.executeHardcoded(method) ?: throw IllegalStateException("No hardcoded for $method") + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt index 63c11cd2..764ed0a9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt @@ -37,7 +37,6 @@ import java.util.function.Predicate import kotlin.concurrent.withLock abstract class AggregatedUpstream( - val targets: CallMethods, val objectMapper: ObjectMapper ): Upstream, Lifecycle { @@ -48,11 +47,20 @@ abstract class AggregatedUpstream( ) var cache: CachingEthereumApi = CachingEthereumApi.empty() private val reconfigLock = ReentrantLock() + private var callMethods: CallMethods = DirectCallMethods() abstract fun getAll(): List abstract fun addUpstream(upstream: Upstream) abstract fun getApis(matcher: Selector.Matcher): Iterator + fun reconfigure() { + reconfigLock.withLock { + getAll().map { it.getMethods() }.let { + callMethods = AggregatedCallMethods(it) + } + } + } + override fun observeStatus(): Flux { val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } } return Flux.merge(upstreamsFluxes) @@ -60,16 +68,8 @@ abstract class AggregatedUpstream( .map { it.status } } - override fun getSupportedTargets(): Set { - val list = HashSet() - getAll().forEach { - list.addAll(it.getSupportedTargets()) - } - return list - } - - override fun isAvailable(matcher: Selector.Matcher): Boolean { - return getAll().any { it.isAvailable(matcher) } + override fun isAvailable(): Boolean { + return getAll().any { it.isAvailable() } } override fun getStatus(): UpstreamAvailability { @@ -82,6 +82,10 @@ abstract class AggregatedUpstream( return UpstreamsConfig.Options() } + override fun getMethods(): CallMethods { + return callMethods + } + class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now()) class FilterBestAvailability(): Predicate { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallMethods.kt index 7984da6e..4765f893 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallMethods.kt @@ -22,5 +22,5 @@ interface CallMethods { fun isAllowed(method: String): Boolean fun getSupportedMethods(): Set fun isHardcoded(method: String): Boolean - fun hardcoded(method: String): Any + fun executeHardcoded(method: String): Any } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt index 33828e92..6657438d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt @@ -16,6 +16,7 @@ package io.emeraldpay.dshackle.upstream import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumHeadMerge @@ -29,9 +30,8 @@ import java.time.Duration open class ChainUpstreams ( val chain: Chain, private val upstreams: MutableList, - targets: CallMethods, objectMapper: ObjectMapper -) : AggregatedUpstream(targets, objectMapper), Lifecycle { +) : AggregatedUpstream(objectMapper), Lifecycle { private val log = LoggerFactory.getLogger(ChainUpstreams::class.java) private var seq = 0 @@ -99,6 +99,7 @@ open class ChainUpstreams ( override fun addUpstream(upstream: Upstream) { upstreams.add(upstream) head = updateHead() + reconfigure() } override fun getApis(matcher: Selector.Matcher): Iterator { @@ -124,6 +125,10 @@ open class ChainUpstreams ( return 0 } + override fun getLabels(): Collection { + return upstreams.flatMap { it.getLabels() } + } + fun printStatus() { var height: Long? = null try { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ConfiguredUpstreams.kt index 1e690df1..aeb3f713 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ConfiguredUpstreams.kt @@ -76,7 +76,7 @@ open class ConfiguredUpstreams( } val options = (up.options ?: UpstreamsConfig.Options()) .merge(defaultOptions[chain] ?: UpstreamsConfig.Options.getDefaults()) - buildEthereumUpstream(up.connection as UpstreamsConfig.EthereumConnection, chain, options, up.labels) + buildEthereumUpstream(up as UpstreamsConfig.Upstream, chain, options) } } } @@ -119,18 +119,27 @@ open class ConfiguredUpstreams( return defaultOptions } - private fun buildEthereumUpstream(up: UpstreamsConfig.EthereumConnection, + private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream, chain: Chain, - options: UpstreamsConfig.Options, - labels: UpstreamsConfig.Labels) { + options: UpstreamsConfig.Options + ) { + val conn = config.connection!! var rpcApi: DirectEthereumApi? = null val urls = ArrayList() - up.rpc?.let { endpoint -> + val methods = if (config.methods != null) { + ManagedCallMethods(getDefaultMethods(chain), + config.methods!!.enabled.map { it.name }.toSet(), + config.methods!!.disabled.map { it.name }.toSet() + ) + } else { + getDefaultMethods(chain) + } + conn.rpc?.let { endpoint -> val rpcTransport = DefaultRpcTransport(endpoint.url) - up.rpc?.basicAuth?.let { auth -> + conn.rpc?.basicAuth?.let { auth -> rpcTransport.setBasicAuth(auth.username, auth.password) } - up.rpc?.tls?.let { tls -> + conn.rpc?.tls?.let { tls -> tls.ca?.let { ca -> File(ca).inputStream().use { cert -> rpcTransport.setTrustedCertificate(cert) } } @@ -139,12 +148,12 @@ open class ConfiguredUpstreams( rpcApi = DirectEthereumApi( rpcClient, objectMapper, - targetFor(chain) + methods ) urls.add(endpoint.url) } if (rpcApi != null) { - val wsApi: EthereumWs? = up.ws?.let { endpoint -> + val wsApi: EthereumWs? = conn.ws?.let { endpoint -> val wsApi = EthereumWs( endpoint.url, endpoint.origin ?: URI("http://localhost"), @@ -159,7 +168,9 @@ open class ConfiguredUpstreams( } log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}") - val ethereumUpstream = EthereumUpstream(chain, rpcApi!!, wsApi, options, NodeDetailsList.NodeDetails(1, labels), targetFor(chain)) + val ethereumUpstream = EthereumUpstream(chain, rpcApi!!, wsApi, options, + NodeDetailsList.NodeDetails(1, config.labels), + methods) ethereumUpstream.start() addUpstream(chain, ethereumUpstream) } @@ -171,8 +182,7 @@ open class ConfiguredUpstreams( endpoint.host!!, endpoint.port ?: 443, objectMapper, - up.auth, - this + up.auth ) log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}") ds.start() @@ -189,7 +199,7 @@ open class ConfiguredUpstreams( override fun addUpstream(chain: Chain, up: Upstream): ChainUpstreams { val current = chainMapping[chain] if (current == null) { - val created = ChainUpstreams(chain, ArrayList(), targetFor(chain), objectMapper) + val created = ChainUpstreams(chain, ArrayList(), objectMapper) created.addUpstream(up) created.start() chainMapping[chain] = created @@ -217,7 +227,7 @@ open class ConfiguredUpstreams( ) } - override fun targetFor(chain: Chain): CallMethods { + override fun getDefaultMethods(chain: Chain): CallMethods { var current = callTargets[chain] if (current == null) { current = QuorumBasedMethods(objectMapper, chain) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DirectCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DirectCallMethods.kt index 939bf032..f07e65b8 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DirectCallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DirectCallMethods.kt @@ -17,26 +17,30 @@ package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.CallQuorum +import java.util.* -class DirectCallMethods : CallMethods { +class DirectCallMethods(private val methods: Set) : CallMethods { + + constructor(): this(emptySet()) + constructor(methods: Collection): this(methods.toSet()) override fun getQuorumFor(method: String): CallQuorum { return AlwaysQuorum() } override fun isAllowed(method: String): Boolean { - return true + return methods.contains(method) } override fun getSupportedMethods(): Set { - return emptySet() + return methods } override fun isHardcoded(method: String): Boolean { return false } - override fun hardcoded(method: String): Any { + override fun executeHardcoded(method: String): Any { return "unsupported" } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteringApiIterator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteringApiIterator.kt index 5ade8e4c..a1eb48a5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteringApiIterator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteringApiIterator.kt @@ -37,7 +37,7 @@ class FilteringApiIterator( return false } val upstream = upstreams[pos++ % upstreams.size] - if (upstream.isAvailable(matcher)) { + if (upstream.isAvailable() && matcher.matches(upstream)) { nextUpstream = upstream } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ManagedCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ManagedCallMethods.kt new file mode 100644 index 00000000..0ac1c3a0 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ManagedCallMethods.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2019 ETCDEV GmbH + * + * 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.upstream + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import io.emeraldpay.dshackle.quorum.CallQuorum +import java.util.* + +class ManagedCallMethods( + private val delegate: CallMethods, + private val enabled: Set, + private val disabled: Set +): CallMethods { + + private val allAllowed: Set = Collections.unmodifiableSet( + enabled + delegate.getSupportedMethods() - disabled + ) + + override fun getQuorumFor(method: String): CallQuorum { + return if (enabled.contains(method)) { + AlwaysQuorum() + } else { + delegate.getQuorumFor(method) + } + } + + override fun isAllowed(method: String): Boolean { + return allAllowed.contains(method) + } + + override fun getSupportedMethods(): Set { + return allAllowed + } + + override fun isHardcoded(method: String): Boolean { + return delegate.isHardcoded(method) + } + + override fun executeHardcoded(method: String): Any { + return delegate.executeHardcoded(method) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/QuorumBasedMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/QuorumBasedMethods.kt index 48eaa950..82bc9535 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/QuorumBasedMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/QuorumBasedMethods.kt @@ -101,7 +101,7 @@ class QuorumBasedMethods( return hardcodedMethods.contains(method) } - override fun hardcoded(method: String): Any { + override fun executeHardcoded(method: String): Any { if ("net_version" == method) { if (Chain.ETHEREUM == chain) { return "1" diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt index 2e727212..76170fbe 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt @@ -19,6 +19,7 @@ import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.dshackle.config.UpstreamsConfig import org.apache.commons.lang3.StringUtils import java.util.* +import kotlin.collections.ArrayList class Selector { @@ -27,9 +28,9 @@ class Selector { val empty = EmptyMatcher() @JvmStatic - fun convertToMatcher(req: BlockchainOuterClass.Selector?): Matcher { + fun convertToMatcher(req: BlockchainOuterClass.Selector?): LabelSelectorMatcher { return when { - req == null -> EmptyMatcher() + req == null -> AnyLabelMatcher() req.hasLabelSelector() -> req.labelSelector.let { selector -> if (StringUtils.isNotEmpty(selector.name)) { val values = selector.valueList @@ -41,24 +42,87 @@ class Selector { LabelMatcher(selector.name, selector.valueList) } } else { - EmptyMatcher() + AnyLabelMatcher() } } req.hasAndSelector() -> AndMatcher(Collections.unmodifiableCollection(req.andSelector.selectorsList.map { convertToMatcher(it) })) req.hasOrSelector() -> OrMatcher(Collections.unmodifiableCollection(req.orSelector.selectorsList.map { convertToMatcher(it) })) req.hasNotSelector() -> NotMatcher(convertToMatcher(req.notSelector.selector)) req.hasExistsSelector() -> ExistsMatcher(req.existsSelector.name) - else -> EmptyMatcher() + else -> AnyLabelMatcher() } } + + @JvmStatic + fun extractLabels(matcher: Matcher): LabelSelectorMatcher? { + if (matcher is LabelSelectorMatcher) { + return matcher + } + if (matcher is MultiMatcher) { + return matcher.getLabelMatcher() + } + return null + } + } + + class Builder { + private val matchers = ArrayList() + + fun forMethod(name: String): Builder { + matchers.add(MethodMatcher(name)) + return this + } + + fun forLabels(matcher: LabelSelectorMatcher): Builder { + matchers.add(matcher) + return this + } + + fun build(): Matcher { + return MultiMatcher(matchers) + } } interface Matcher { - fun matches(labels: UpstreamsConfig.Labels): Boolean - fun asProto(): BlockchainOuterClass.Selector? + fun matches(up: Upstream): Boolean + } + + class MultiMatcher( + private val matchers: Collection + ): Matcher { + override fun matches(up: Upstream): Boolean { + return matchers.all { it.matches(up) } + } + + fun getLabelMatcher(): LabelSelectorMatcher? { + return matchers.find { it is LabelSelectorMatcher } as LabelSelectorMatcher? + } + } + + class MethodMatcher( + val method: String + ): Matcher { + override fun matches(up: Upstream): Boolean { + return up.getMethods().isAllowed(method) + } + } + + abstract class LabelSelectorMatcher: Matcher { + override fun matches(up: Upstream): Boolean { + return up.getLabels().any(this::matches) + } + abstract fun matches(labels: UpstreamsConfig.Labels): Boolean + abstract fun asProto(): BlockchainOuterClass.Selector? } class EmptyMatcher: Matcher { + override fun matches(up: Upstream): Boolean { + return true + } + } + + class AnyLabelMatcher: LabelSelectorMatcher() { + override fun matches(labels: UpstreamsConfig.Labels): Boolean { return true } @@ -66,9 +130,13 @@ class Selector { override fun asProto(): BlockchainOuterClass.Selector? { return null } + + override fun matches(up: Upstream): Boolean { + return true + } } - class LabelMatcher(val name: String, val values: Collection): Matcher { + class LabelMatcher(val name: String, val values: Collection): LabelSelectorMatcher() { override fun matches(labels: UpstreamsConfig.Labels): Boolean { return labels.get(name)?.let { labelValue -> values.any { it == labelValue } @@ -84,7 +152,7 @@ class Selector { } } - class OrMatcher(val matchers: Collection): Matcher { + class OrMatcher(val matchers: Collection): LabelSelectorMatcher() { override fun matches(labels: UpstreamsConfig.Labels): Boolean { return matchers.any { matcher -> matcher.matches(labels) } } @@ -98,7 +166,7 @@ class Selector { } } - class AndMatcher(val matchers: Collection): Matcher { + class AndMatcher(val matchers: Collection): LabelSelectorMatcher() { override fun matches(labels: UpstreamsConfig.Labels): Boolean { return matchers.all { matcher -> matcher.matches(labels) } } @@ -112,7 +180,7 @@ class Selector { } } - class NotMatcher(val matcher: Matcher): Matcher { + class NotMatcher(val matcher: LabelSelectorMatcher): LabelSelectorMatcher() { override fun matches(labels: UpstreamsConfig.Labels): Boolean { return !matcher.matches(labels) } @@ -126,7 +194,7 @@ class Selector { } } - class ExistsMatcher(val name: String): Matcher { + class ExistsMatcher(val name: String): LabelSelectorMatcher() { override fun matches(labels: UpstreamsConfig.Labels): Boolean { return labels.containsKey(name) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt index 95f92bde..b2c3a703 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt @@ -21,14 +21,14 @@ import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import reactor.core.publisher.Flux interface Upstream { - fun isAvailable(matcher: Selector.Matcher): Boolean + fun isAvailable(): Boolean fun getStatus(): UpstreamAvailability fun observeStatus(): Flux fun getHead(): EthereumHead fun getApi(matcher: Selector.Matcher): DirectEthereumApi -// fun getCache(): CachingEthereumApi fun getOptions(): UpstreamsConfig.Options - fun getSupportedTargets(): Set fun setLag(lag: Long) fun getLag(): Long + fun getLabels(): Collection + fun getMethods(): CallMethods } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt index 32f29258..99ad5d9d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt @@ -23,6 +23,6 @@ interface Upstreams { fun getUpstream(chain: Chain): AggregatedUpstream? fun getAvailable(): List fun observeChains(): Flux - fun targetFor(chain: Chain): CallMethods + fun getDefaultMethods(chain: Chain): CallMethods fun isAvailable(chain: Chain): Boolean } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApi.kt index 0bc726c4..fe0151ad 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApi.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApi.kt @@ -36,7 +36,7 @@ open class DirectEthereumApi( override fun execute(id: Int, method: String, params: List): Mono { val result: Mono = when { - targets.isHardcoded(method) -> Mono.just(method).map { targets.hardcoded(it) } + targets.isHardcoded(method) -> Mono.just(method).map { targets.executeHardcoded(it) } targets.isAllowed(method) -> callUpstream(method, params) else -> Mono.error(RpcException(-32601, "Method not allowed or not found")) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt index 149f265b..836422b5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt @@ -35,9 +35,6 @@ open class EthereumUpstream( UpstreamsConfig.Options.getDefaults(), NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels()), DirectCallMethods()) - override fun getSupportedTargets(): Set { - return targets.getSupportedMethods() - } private val log = LoggerFactory.getLogger(EthereumUpstream::class.java) @@ -92,8 +89,8 @@ open class EthereumUpstream( } } - override fun isAvailable(matcher: Selector.Matcher): Boolean { - return getStatus() == UpstreamAvailability.OK && matcher.matches(node.labels) + override fun isAvailable(): Boolean { + return getStatus() == UpstreamAvailability.OK } override fun getHead(): EthereumHead { @@ -112,4 +109,12 @@ open class EthereumUpstream( return options } + override fun getLabels(): Collection { + return listOf(node.labels) + } + + override fun getMethods(): CallMethods { + return targets + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransport.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransport.kt index b2f5aa5a..584eee0e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransport.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransport.kt @@ -36,7 +36,7 @@ import java.util.function.Function class EthereumGrpcTransport( private val chainRef: Common.ChainRef, - private val selector: BlockchainOuterClass.Selector?, + private val labelSelector: BlockchainOuterClass.Selector?, private val client: ReactorBlockchainGrpc.ReactorBlockchainStub, private val objectMapper: ObjectMapper ): RpcTransport { @@ -47,13 +47,13 @@ class EthereumGrpcTransport( chain: Chain, client: ReactorBlockchainGrpc.ReactorBlockchainStub, objectMapper: ObjectMapper - ) : this(Common.ChainRef.forNumber(chain.id), Selector.EmptyMatcher().asProto(), client, objectMapper) + ) : this(Common.ChainRef.forNumber(chain.id), null, client, objectMapper) - fun withMatcher(matcher: Selector.Matcher): EthereumGrpcTransport { - if (matcher is Selector.EmptyMatcher && selector == null) { + fun withLabels(matcher: Selector.LabelSelectorMatcher?): EthereumGrpcTransport { + if ((matcher == null || matcher is Selector.AnyLabelMatcher) && labelSelector == null) { return this } - return EthereumGrpcTransport(chainRef, matcher.asProto(), client, objectMapper) + return EthereumGrpcTransport(chainRef, matcher?.asProto(), client, objectMapper) } override fun close() { @@ -114,8 +114,8 @@ class EthereumGrpcTransport( override fun execute(items: List>): CompletableFuture { val req = BlockchainOuterClass.NativeCallRequest.newBuilder() .setChain(chainRef) - if (selector != null) { - req.setSelector(selector) + if (labelSelector != null) { + req.setSelector(labelSelector) } val mapping = prepareMapping(items, req) return client.nativeCall(req.build()) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstream.kt index 3857f21a..a3b0fc09 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstream.kt @@ -42,14 +42,15 @@ import java.time.Duration import java.util.* import java.util.concurrent.atomic.AtomicReference import java.util.function.Function +import kotlin.collections.ArrayList open class GrpcUpstream( private val chain: Chain, private val client: ReactorBlockchainGrpc.ReactorBlockchainStub, - private val objectMapper: ObjectMapper, - private val targets: CallMethods + private val objectMapper: ObjectMapper ): DefaultUpstream(), Lifecycle { + private var allLabels: Collection = ArrayList() private val log = LoggerFactory.getLogger(GrpcUpstream::class.java) private val options = UpstreamsConfig.Options.getDefaults() @@ -57,13 +58,13 @@ open class GrpcUpstream( private val streamBlocks: TopicProcessor> = TopicProcessor.create() private val nodes = AtomicReference(NodeDetailsList()) private val head = Head(this) - private val supportedMethods = HashSet() + private var targets: CallMethods = DirectCallMethods() private val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper) private var headSubscription: Disposable? = null open fun createApi(matcher: Selector.Matcher): DirectEthereumApi { - val rpcClient = DefaultRpcClient(grpcTransport.withMatcher(matcher)) + val rpcClient = DefaultRpcClient(grpcTransport.withLabels(Selector.extractLabels(matcher))) return DirectEthereumApi(rpcClient, objectMapper, targets).let { it.upstream = this it @@ -130,19 +131,24 @@ open class GrpcUpstream( } fun init(conf: BlockchainOuterClass.DescribeChain) { - supportedMethods.addAll(conf.supportedMethodsList) + targets = DirectCallMethods(conf.supportedMethodsList.toSet()) val nodes = NodeDetailsList() + val allLabels = ArrayList() conf.nodesList.forEach { node -> val node = NodeDetailsList.NodeDetails(node.quorum, node.labelsList.let { provided -> val labels = UpstreamsConfig.Labels() - provided.forEach { labels.put(it.name, it.value) } + provided.forEach { + labels[it.name] = it.value + } + allLabels.add(labels) labels } ) nodes.add(node) } this.nodes.set(nodes) + this.allLabels = Collections.unmodifiableCollection(allLabels) conf.status?.let { status -> onStatus(status) } } @@ -160,13 +166,17 @@ open class GrpcUpstream( // ------------------------------------------------------------------------------------------ - override fun getSupportedTargets(): Set { - return supportedMethods + override fun getLabels(): Collection { + return allLabels } - override fun isAvailable(matcher: Selector.Matcher): Boolean { + override fun getMethods(): CallMethods { + return targets + } + + override fun isAvailable(): Boolean { return getStatus() == UpstreamAvailability.OK && headBlock.get() != null && nodes.get().getNodes().any { - it.quorum > 0 && matcher.matches(it.labels) + it.quorum > 0 } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt index b7de4807..b3c19129 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt @@ -39,8 +39,7 @@ class GrpcUpstreams( private val host: String, private val port: Int, private val objectMapper: ObjectMapper, - private val auth: UpstreamsConfig.TlsAuth? = null, - private val upstreams: Upstreams + private val auth: UpstreamsConfig.TlsAuth? = null ) { private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java) @@ -111,7 +110,7 @@ class GrpcUpstreams( lock.withLock { val current = known[chain] return if (current == null) { - val created = GrpcUpstream(chain, client!!, objectMapper, upstreams.targetFor(chain)) + val created = GrpcUpstream(chain, client!!, objectMapper) known[chain] = created created.start() created diff --git a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy index 390c882f..9d0f7653 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy @@ -174,4 +174,24 @@ class UpstreamsConfigReaderSpec extends Specification { } } } + + def "Parse config with methods"() { + setup: + def config = this.class.getClassLoader().getResourceAsStream("upstreams-methods.yaml") + when: + def act = reader.read(config) + then: + act != null + with(act.upstreams.get(0)) { + methods != null + with(methods) { + enabled.size() == 1 + enabled.first().name == "parity_trace" + + disabled.size() == 2 + disabled.toList()[0].name == "eth_getBlockByNumber" + disabled.toList()[1].name == "admin_shutdown" + } + } + } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy index 68ecc6cc..40794718 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy @@ -15,6 +15,10 @@ */ package io.emeraldpay.dshackle.test +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.CallMethods +import io.emeraldpay.dshackle.upstream.NodeDetailsList +import io.emeraldpay.dshackle.upstream.QuorumBasedMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream @@ -29,7 +33,13 @@ class EthereumUpstreamMock extends EthereumUpstream { EthereumHeadMock ethereumHeadMock = new EthereumHeadMock() EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) { - super(chain, api) + this(chain, api, new QuorumBasedMethods(TestingCommons.objectMapper(), chain)) + } + + EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) { + super(chain, api, null, + UpstreamsConfig.Options.getDefaults(), new NodeDetailsList.NodeDetails(1, new UpstreamsConfig.Labels()), + methods) setLag(0) setStatus(UpstreamAvailability.OK) } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy index bfde8fab..f507f5e9 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy @@ -20,9 +20,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import io.emeraldpay.dshackle.upstream.AggregatedUpstream +import io.emeraldpay.dshackle.upstream.CallMethods import io.emeraldpay.dshackle.upstream.ChainUpstreams import io.emeraldpay.dshackle.upstream.DirectCallMethods +import io.emeraldpay.dshackle.upstream.QuorumBasedMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi +import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.RpcClient @@ -57,6 +60,10 @@ class TestingCommons { } static AggregatedUpstream aggregatedUpstream(DirectEthereumApi api) { - return new ChainUpstreams(Chain.ETHEREUM, [upstream(api)], new DirectCallMethods(), objectMapper()) + return aggregatedUpstream(upstream(api)) + } + + static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) { + return new ChainUpstreams(Chain.ETHEREUM, [up], objectMapper()) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/UpstreamsMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/UpstreamsMock.groovy index 36a8a7a0..5107ac9f 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/UpstreamsMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/UpstreamsMock.groovy @@ -41,7 +41,7 @@ class UpstreamsMock implements Upstreams { @Override AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull Upstream up) { if (!upstreams.containsKey(chain)) { - upstreams[chain] = new ChainUpstreams(chain, [up], targetFor(chain), TestingCommons.objectMapper()) + upstreams[chain] = new ChainUpstreams(chain, [up], TestingCommons.objectMapper()) } else { upstreams[chain].addUpstream(up) } @@ -64,7 +64,7 @@ class UpstreamsMock implements Upstreams { } @Override - QuorumBasedMethods targetFor(@NotNull Chain chain) { + QuorumBasedMethods getDefaultMethods(@NotNull Chain chain) { if (target[chain] == null) { QuorumBasedMethods targets = new QuorumBasedMethods(TestingCommons.objectMapper(), chain) target[chain] = targets diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/AggregatedCallMethodsSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/AggregatedCallMethodsSpec.groovy new file mode 100644 index 00000000..aad02fa2 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/AggregatedCallMethodsSpec.groovy @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2019 ETCDEV GmbH + * + * 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.upstream + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import spock.lang.Specification + +class AggregatedCallMethodsSpec extends Specification { + + def "Returns quorum from delegate that owns it"() { + setup: + def quorum = new AlwaysQuorum() + def delegate1 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_no_test", "foo_bar"] + 1 * isAllowed("eth_test") >> false + } + def delegate2 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_test", "foo_bar"] + 1 * isAllowed("eth_test") >> true + 1 * getQuorumFor("eth_test") >> quorum + } + def aggregate = new AggregatedCallMethods([delegate1, delegate2]) + when: + def act = aggregate.getQuorumFor("eth_test") + then: + act == quorum + } + + def "Allowed if any allowed"() { + setup: + def delegate1 = new DirectCallMethods(["eth_no_test", "foo_bar"] as Set) + def delegate2 = new DirectCallMethods(["eth_test", "foo_bar"] as Set) + def aggregate = new AggregatedCallMethods([delegate1, delegate2]) + when: + def act = aggregate.isAllowed("eth_test") + then: + act + + when: + act = aggregate.isAllowed("eth_no_test") + then: + act + + when: + act = aggregate.isAllowed("foo_bar") + then: + act + + when: + act = aggregate.isAllowed("nothing") + then: + !act + } + + def "Supported has all methods"() { + setup: + def delegate1 = new DirectCallMethods(["eth_no_test", "foo_bar"] as Set) + def delegate2 = new DirectCallMethods(["eth_test", "foo_bar"] as Set) + def aggregate = new AggregatedCallMethods([delegate1, delegate2]) + when: + def act = aggregate.getSupportedMethods() + then: + act.sort() == ["eth_test", "eth_no_test", "foo_bar"].sort() + } + + def "Hardcoded if any hardcoded"() { + setup: + def delegate1 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_no_test", "foo_bar"] + 1 * isAllowed("eth_test") >> false + 1 * isAllowed("eth_no_test") >> true + + 1 * isHardcoded("eth_no_test") >> false + } + def delegate2 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_test", "foo_bar"] + 1 * isAllowed("eth_test") >> true + 1 * isAllowed("eth_no_test") >> false + + 1 * isHardcoded("eth_test") >> true + } + def aggregate = new AggregatedCallMethods([delegate1, delegate2]) + when: + def act = aggregate.isHardcoded("eth_test") + then: + act + + when: + act = aggregate.isHardcoded("eth_no_test") + then: + !act + } + + def "Execute hardcoded on delegate that owns it"() { + setup: + def delegate1 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_no_test", "foo_bar"] + 1 * isAllowed("eth_test") >> false + } + def delegate2 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_test", "foo_bar"] + 1 * isAllowed("eth_test") >> true + 1 * isHardcoded("eth_test") >> true + 1 * executeHardcoded("eth_test") >> "hello" + } + def aggregate = new AggregatedCallMethods([delegate1, delegate2]) + when: + def act = aggregate.executeHardcoded("eth_test") + then: + act == "hello" + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ManagedCallMethodsSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ManagedCallMethodsSpec.groovy new file mode 100644 index 00000000..0f3b5301 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ManagedCallMethodsSpec.groovy @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2019 ETCDEV GmbH + * + * 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.upstream + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import spock.lang.Specification + +class ManagedCallMethodsSpec extends Specification { + + def "Gets quorum for enabled method"() { + setup: + def managed = new ManagedCallMethods( + new DirectCallMethods(), + ["eth_test"] as Set, + [] as Set + ) + when: + def act = managed.getQuorumFor("eth_test") + then: + act instanceof AlwaysQuorum + } + + def "Allowed contacts all enabled + delegate"() { + setup: + def managed = new ManagedCallMethods( + new DirectCallMethods(["eth_test2"] as Set), + ["eth_test"] as Set, + [] as Set + ) + when: + def act = managed.getSupportedMethods() + then: + act.sort() == ["eth_test", "eth_test2"].sort() + } + + def "Disabled removed from delegate"() { + setup: + def managed = new ManagedCallMethods( + new DirectCallMethods(["eth_test2", "foo_bar"] as Set), + ["eth_test"] as Set, + ["foo_bar"] as Set + ) + when: + def act = managed.getSupportedMethods() + then: + act.sort() == ["eth_test", "eth_test2"].sort() + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy index 0f884f8b..0b86754d 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/SelectorSpec.groovy @@ -35,7 +35,7 @@ class SelectorSpec extends Specification { when: def act = Selector.convertToMatcher(null) then: - act.class == Selector.EmptyMatcher + act.class == Selector.AnyLabelMatcher } def "Convert LABEL match"() { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransportSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransportSpec.groovy index c5817186..13e5a5a7 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransportSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcTransportSpec.groovy @@ -20,8 +20,10 @@ import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.ReactorBlockchainGrpc import io.emeraldpay.dshackle.rpc.NativeCall import io.emeraldpay.dshackle.test.EthereumApiMock +import io.emeraldpay.dshackle.test.EthereumUpstreamMock import io.emeraldpay.dshackle.test.MockServer import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.DirectCallMethods import io.emeraldpay.dshackle.upstream.QuorumBasedMethods import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.grpc.EthereumGrpcTransport @@ -45,7 +47,9 @@ class EthereumGrpcTransportSpec extends Specification { def callData = [:] def otherSideUpstreams = Mock(Upstreams) - def otherSideAggr = TestingCommons.aggregatedUpstream(otherSideApi) + def otherSideAggr = TestingCommons.aggregatedUpstream( + new EthereumUpstreamMock(Chain.ETHEREUM, otherSideApi, new DirectCallMethods(["eth_test"])) + ) def otherSideNativeCall = new NativeCall(otherSideUpstreams, objectMapper) otherSideApi.upstream = otherSideAggr @@ -88,7 +92,9 @@ class EthereumGrpcTransportSpec extends Specification { def callData = [:] def otherSideUpstreams = Mock(Upstreams) - def otherSideAggr = TestingCommons.aggregatedUpstream(otherSideApi) + def otherSideAggr = TestingCommons.aggregatedUpstream( + new EthereumUpstreamMock(Chain.ETHEREUM, otherSideApi, new DirectCallMethods(["eth_test", "eth_test2"])) + ) def otherSideNativeCall = new NativeCall(otherSideUpstreams, objectMapper) otherSideApi.upstream = otherSideAggr diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreamSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreamSpec.groovy index f26640d9..d46d2292 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreamSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreamSpec.groovy @@ -39,7 +39,6 @@ class GrpcUpstreamSpec extends Specification { MockServer mockServer = new MockServer() ObjectMapper objectMapper = TestingCommons.objectMapper() - def ethereumTargets = new QuorumBasedMethods(objectMapper, Chain.ETHEREUM) def "Subscribe to head"() { setup: @@ -71,8 +70,11 @@ class GrpcUpstreamSpec extends Specification { ) } }) - def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets) + def upstream = new GrpcUpstream(chain, client, objectMapper) upstream.setLag(0) + upstream.init(BlockchainOuterClass.DescribeChain.newBuilder() + .addAllSupportedMethods(["eth_getBlockByHash"]) + .build()) when: upstream.start() def h = upstream.head.getFlux().next().block(Duration.ofSeconds(1)) @@ -127,8 +129,11 @@ class GrpcUpstreamSpec extends Specification { finished.complete(true) } }) - def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets) + def upstream = new GrpcUpstream(chain, client, objectMapper) upstream.setLag(0) + upstream.init(BlockchainOuterClass.DescribeChain.newBuilder() + .addAllSupportedMethods(["eth_getBlockByHash"]) + .build()) when: upstream.start() finished.get() @@ -184,8 +189,11 @@ class GrpcUpstreamSpec extends Specification { finished.complete(true) } }) - def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets) + def upstream = new GrpcUpstream(chain, client, objectMapper) upstream.setLag(0) + upstream.init(BlockchainOuterClass.DescribeChain.newBuilder() + .addAllSupportedMethods(["eth_getBlockByHash"]) + .build()) when: upstream.start() finished.get() diff --git a/src/test/resources/upstreams-methods.yaml b/src/test/resources/upstreams-methods.yaml new file mode 100644 index 00000000..f5569404 --- /dev/null +++ b/src/test/resources/upstreams-methods.yaml @@ -0,0 +1,26 @@ +version: v1 + +defaultOptions: + - chains: + - ethereum + options: + min-peers: 3 + +upstreams: + - id: local + chain: ethereum + options: + min-peers: 7 + methods: + enabled: + - name: "parity_trace" + disabled: + - name: "eth_getBlockByNumber" + - name: "admin_shutdown" + connection: + ethereum: + rpc: + url: "http://localhost:8545" + ws: + url: "ws://localhost:8546" + origin: "http://localhost" \ No newline at end of file