solution: setup custom upstream methods
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<Method>,
|
||||
val disabled: Set<Method>
|
||||
)
|
||||
|
||||
class Method(
|
||||
val name: String
|
||||
)
|
||||
}
|
||||
@@ -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<UpstreamsConfig.GrpcConnection>) {
|
||||
@@ -139,6 +140,29 @@ class UpstreamsConfigReader {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun tryReadMethods(upNode: MappingNode): UpstreamsConfig.Methods? {
|
||||
return getMapping(upNode, "methods")?.let { mnode ->
|
||||
val enabled = getList<MappingNode>(mnode, "enabled")?.value?.map { m ->
|
||||
getValueAsString(m, "name")?.let { name ->
|
||||
UpstreamsConfig.Method(
|
||||
name = name
|
||||
)
|
||||
}
|
||||
}?.filterNotNull()?.toSet() ?: emptySet()
|
||||
val disabled = getList<MappingNode>(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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -95,11 +95,16 @@ class NativeCall(
|
||||
}
|
||||
|
||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux<CallContext<RawCallDetails>> {
|
||||
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<Any>
|
||||
}
|
||||
|
||||
open class CallContext<T>(val id: Int, val upstream: AggregatedUpstream, val matcher: Selector.Matcher, val callQuorum: CallQuorum, val payload: T) {
|
||||
open class CallContext<T>(val id: Int,
|
||||
val upstream: AggregatedUpstream,
|
||||
val matcher: Selector.Matcher,
|
||||
val callQuorum: CallQuorum,
|
||||
val payload: T) {
|
||||
fun <X> withPayload(payload: X): CallContext<X> {
|
||||
return CallContext(id, upstream, matcher, callQuorum, payload)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
): CallMethods {
|
||||
|
||||
private val allMethods: Set<String>
|
||||
|
||||
init {
|
||||
val buf = HashSet<String>()
|
||||
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<String> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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<Upstream>
|
||||
abstract fun addUpstream(upstream: Upstream)
|
||||
abstract fun getApis(matcher: Selector.Matcher): Iterator<DirectEthereumApi>
|
||||
|
||||
fun reconfigure() {
|
||||
reconfigLock.withLock {
|
||||
getAll().map { it.getMethods() }.let {
|
||||
callMethods = AggregatedCallMethods(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||
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<String> {
|
||||
val list = HashSet<String>()
|
||||
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<UpstreamStatus> {
|
||||
|
||||
@@ -22,5 +22,5 @@ interface CallMethods {
|
||||
fun isAllowed(method: String): Boolean
|
||||
fun getSupportedMethods(): Set<String>
|
||||
fun isHardcoded(method: String): Boolean
|
||||
fun hardcoded(method: String): Any
|
||||
fun executeHardcoded(method: String): Any
|
||||
}
|
||||
@@ -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<Upstream>,
|
||||
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<DirectEthereumApi> {
|
||||
@@ -124,6 +125,10 @@ open class ChainUpstreams (
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||
return upstreams.flatMap { it.getLabels() }
|
||||
}
|
||||
|
||||
fun printStatus() {
|
||||
var height: Long? = null
|
||||
try {
|
||||
|
||||
@@ -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<UpstreamsConfig.EthereumConnection>, chain, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,18 +119,27 @@ open class ConfiguredUpstreams(
|
||||
return defaultOptions
|
||||
}
|
||||
|
||||
private fun buildEthereumUpstream(up: UpstreamsConfig.EthereumConnection,
|
||||
private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>,
|
||||
chain: Chain,
|
||||
options: UpstreamsConfig.Options,
|
||||
labels: UpstreamsConfig.Labels) {
|
||||
options: UpstreamsConfig.Options
|
||||
) {
|
||||
val conn = config.connection!!
|
||||
var rpcApi: DirectEthereumApi? = null
|
||||
val urls = ArrayList<URI>()
|
||||
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<Upstream>(), targetFor(chain), objectMapper)
|
||||
val created = ChainUpstreams(chain, ArrayList<Upstream>(), 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)
|
||||
|
||||
@@ -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<String>) : CallMethods {
|
||||
|
||||
constructor(): this(emptySet())
|
||||
constructor(methods: Collection<String>): 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<String> {
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
private val disabled: Set<String>
|
||||
): CallMethods {
|
||||
|
||||
private val allAllowed: Set<String> = 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<String> {
|
||||
return allAllowed
|
||||
}
|
||||
|
||||
override fun isHardcoded(method: String): Boolean {
|
||||
return delegate.isHardcoded(method)
|
||||
}
|
||||
|
||||
override fun executeHardcoded(method: String): Any {
|
||||
return delegate.executeHardcoded(method)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Matcher>()
|
||||
|
||||
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>
|
||||
): 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<String>): Matcher {
|
||||
class LabelMatcher(val name: String, val values: Collection<String>): 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>): Matcher {
|
||||
class OrMatcher(val matchers: Collection<LabelSelectorMatcher>): 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>): Matcher {
|
||||
class AndMatcher(val matchers: Collection<LabelSelectorMatcher>): 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)
|
||||
}
|
||||
|
||||
@@ -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<UpstreamAvailability>
|
||||
fun getHead(): EthereumHead
|
||||
fun getApi(matcher: Selector.Matcher): DirectEthereumApi
|
||||
// fun getCache(): CachingEthereumApi
|
||||
fun getOptions(): UpstreamsConfig.Options
|
||||
fun getSupportedTargets(): Set<String>
|
||||
fun setLag(lag: Long)
|
||||
fun getLag(): Long
|
||||
fun getLabels(): Collection<UpstreamsConfig.Labels>
|
||||
fun getMethods(): CallMethods
|
||||
}
|
||||
@@ -23,6 +23,6 @@ interface Upstreams {
|
||||
fun getUpstream(chain: Chain): AggregatedUpstream?
|
||||
fun getAvailable(): List<Chain>
|
||||
fun observeChains(): Flux<Chain>
|
||||
fun targetFor(chain: Chain): CallMethods
|
||||
fun getDefaultMethods(chain: Chain): CallMethods
|
||||
fun isAvailable(chain: Chain): Boolean
|
||||
}
|
||||
@@ -36,7 +36,7 @@ open class DirectEthereumApi(
|
||||
|
||||
override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
||||
val result: Mono<out Any> = 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"))
|
||||
}
|
||||
|
||||
@@ -35,9 +35,6 @@ open class EthereumUpstream(
|
||||
UpstreamsConfig.Options.getDefaults(), NodeDetailsList.NodeDetails(1, UpstreamsConfig.Labels()),
|
||||
DirectCallMethods())
|
||||
|
||||
override fun getSupportedTargets(): Set<String> {
|
||||
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<UpstreamsConfig.Labels> {
|
||||
return listOf(node.labels)
|
||||
}
|
||||
|
||||
override fun getMethods(): CallMethods {
|
||||
return targets
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Batch.BatchItem<out Any, out Any>>): CompletableFuture<BatchStatus> {
|
||||
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())
|
||||
|
||||
@@ -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<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
|
||||
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<BlockJson<TransactionId>> = TopicProcessor.create()
|
||||
private val nodes = AtomicReference<NodeDetailsList>(NodeDetailsList())
|
||||
private val head = Head(this)
|
||||
private val supportedMethods = HashSet<String>()
|
||||
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<UpstreamsConfig.Labels>()
|
||||
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<String> {
|
||||
return supportedMethods
|
||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"() {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
26
src/test/resources/upstreams-methods.yaml
Normal file
26
src/test/resources/upstreams-methods.yaml
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user