From 101578aa5f819fa29e5169f3559b8e63ab84612f Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Sun, 26 Apr 2020 20:35:19 -0400 Subject: [PATCH] solution: methods configuration for bitcoin --- .../dshackle/startup/ConfiguredUpstreams.kt | 28 ++++--- .../dshackle/upstream/CurrentUpstreams.kt | 7 +- .../dshackle/upstream/bitcoin/BitcoinApi.kt | 62 +++++++++++++++- .../upstream/bitcoin/BitcoinUpstream.kt | 7 +- .../upstream/bitcoin/DefaultBitcoinMethods.kt | 74 ++++++++++++++++--- .../upstream/bitcoin/BitcoinApiSpec.groovy | 2 +- 6 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index 02eb54bb..4f2735b4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -23,6 +23,8 @@ import io.emeraldpay.dshackle.upstream.CurrentUpstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream +import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods +import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream @@ -116,22 +118,33 @@ open class ConfiguredUpstreams( return defaultOptions } + private fun buildMethods(config: UpstreamsConfig.Upstream<*>, chain: Chain): CallMethods { + return if (config.methods != null) { + ManagedCallMethods(currentUpstreams.getDefaultMethods(chain), + config.methods!!.enabled.map { it.name }.toSet(), + config.methods!!.disabled.map { it.name }.toSet() + ) + } else { + currentUpstreams.getDefaultMethods(chain) + } + } + private fun buildBitcoinUpstream(config: UpstreamsConfig.Upstream, chain: Chain, options: UpstreamsConfig.Options) { val conn = config.connection!! var rpcApi: BitcoinApi? = null - + val methods = buildMethods(config, chain) conn.rpc?.let { endpoint -> val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) - rpcApi = BitcoinApi(rpcClient, objectMapper) + rpcApi = BitcoinApi(rpcClient, objectMapper, methods) } rpcApi?.let { api -> val upstream = BitcoinUpstream(config.id ?: "bitcoin-${seq.getAndIncrement()}", chain, api, options, QuorumForLabels.QuorumItem(1, config.labels), - objectMapper) + objectMapper, methods) upstream.start() currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED)) @@ -145,14 +158,7 @@ open class ConfiguredUpstreams( val conn = config.connection!! var rpcApi: DirectEthereumApi? = null val urls = ArrayList() - val methods = if (config.methods != null) { - ManagedCallMethods(currentUpstreams.getDefaultMethods(chain), - config.methods!!.enabled.map { it.name }.toSet(), - config.methods!!.disabled.map { it.name }.toSet() - ) - } else { - currentUpstreams.getDefaultMethods(chain) - } + val methods = buildMethods(config, chain) conn.rpc?.let { endpoint -> val rpcClient = ReactorHttpRpcClient.newBuilder() .connectTo(endpoint.url) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt index 666a1e73..8d0733cf 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt @@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.startup.UpstreamChange import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream +import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi @@ -136,7 +137,11 @@ class CurrentUpstreams( } fun setupDefaultMethods(chain: Chain): CallMethods { - val created = DefaultEthereumMethods(objectMapper, chain) + val created = when (BlockchainType.fromBlockchain(chain)) { + BlockchainType.ETHEREUM -> DefaultEthereumMethods(objectMapper, chain) + BlockchainType.BITCOIN -> DefaultBitcoinMethods(objectMapper) + else -> throw IllegalStateException("Unsupported chain: $chain") + } callTargets[chain] = created return created } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt index ae4d22f2..195d7880 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt @@ -3,7 +3,11 @@ package io.emeraldpay.dshackle.upstream.bitcoin import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.UpstreamApi +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.grpc.Status +import io.grpc.StatusRuntimeException import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.json.FullResponseJson import io.infinitape.etherjar.rpc.json.RequestJson import io.infinitape.etherjar.rpc.json.ResponseJson @@ -12,7 +16,8 @@ import reactor.core.publisher.Mono open class BitcoinApi( val bitcoinRpcClient: BitcoinRpcClient, - val objectMapper: ObjectMapper + val objectMapper: ObjectMapper, + val targets: CallMethods ) : UpstreamApi { companion object { @@ -20,10 +25,59 @@ open class BitcoinApi( } open override fun execute(id: Int, method: String, params: List): Mono { - //TODO optimize extraction - return executeAndResult(id, method, params, Object::class.java).map { - objectMapper.writeValueAsBytes(it) + //TODO it's almost the same code as for DirectEthereumApi; refactor + val result: Mono = when { + targets.isHardcoded(method) -> Mono.just(method).map { targets.executeHardcoded(it) } + targets.isAllowed(method) -> executeAndResult(id, method, params, Object::class.java) + else -> Mono.error(RpcException(-32601, "Method not allowed or not found")) } + return processResult(id, method, result) + } + + public fun processResult(id: Int, method: String, result: Mono): Mono { + //TODO it's the same code as for DirectEthereumApi; refactor + return result + .doOnError { t -> + log.warn("Upstream error: [${t.message}] for $method") + } + .map { + val resp = ResponseJson() + resp.id = id + resp.result = it + resp + } + .switchIfEmpty( + Mono.fromCallable { + val resp = ResponseJson() + resp.id = id + resp.result = null + resp + } + ) + .map { + objectMapper.writer().writeValueAsBytes(it) + } + .onErrorResume(StatusRuntimeException::class.java) { t -> + if (t.status.code == Status.Code.CANCELLED) { + Mono.empty() + } else { + Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, "gRPC error ${t.status}")) + } + } + .onErrorMap { t -> + if (RpcException::class.java.isAssignableFrom(t.javaClass)) { + t + } else { + log.warn("Convert to RPC error. Exception ${t.javaClass}:${t.message}", t) + RpcException(-32020, "Error reading from upstream", null, t) + } + } + .onErrorResume(RpcException::class.java) { t -> + val resp = ResponseJson() + resp.id = id + resp.error = t.error + Mono.just(objectMapper.writer().writeValueAsBytes(resp)) + } } open fun executeAndResult(id: Int, method: String, params: List, resultType: Class): Mono { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt index 52753baf..f801cd21 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -4,7 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.* +import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.grpc.Chain +import io.infinitape.etherjar.rpc.JacksonRpcConverter import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable @@ -16,8 +18,9 @@ class BitcoinUpstream( private val api: BitcoinApi, options: UpstreamsConfig.Options, val node: QuorumForLabels.QuorumItem, - private val objectMapper: ObjectMapper -) : DefaultUpstream(id, options, DefaultBitcoinMethods()), Lifecycle { + private val objectMapper: ObjectMapper, + callMethods: CallMethods +) : DefaultUpstream(id, options, callMethods), Lifecycle { companion object { private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt index c6e94d3b..7ec44584 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt @@ -1,15 +1,71 @@ package io.emeraldpay.dshackle.upstream.bitcoin -import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.quorum.* +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.infinitape.etherjar.rpc.JacksonRpcConverter +import io.infinitape.etherjar.rpc.RpcException +import java.util.* -class DefaultBitcoinMethods : DirectCallMethods( - listOf( - "getbestblockhash", "getblock", "getblocknumber", "getblockcount", - "gettransaction", "getrawtransaction", "gettxout", - "getreceivedbyaddress", "listunspent", - "getmemorypool" - ) -) { +class DefaultBitcoinMethods( + private val objectMapper: ObjectMapper +) : CallMethods { + //TODO maybe Ethereum RPC parser should not be really used for Bitcoin + private val jacksonRpcConverter = JacksonRpcConverter(objectMapper) + + private val anyResponseMethods = listOf( + "getblock", + "gettransaction", "getrawtransaction", "gettxout", + "getmemorypool" + ).sorted() + + private val headVerifiedMethods = listOf( + "getbestblockhash", "getblocknumber", "getblockcount", + "listunspent", "getreceivedbyaddress" + ).sorted() + + private val hardcodedMethods = listOf( + "getconnectioncount", "getnetworkinfo" + ).sorted() + + private val broadcastMethods = listOf( + "sendrawtransaction" + ).sorted() + + private val allowedMethods = (anyResponseMethods + hardcodedMethods + headVerifiedMethods).sorted() + + override fun getQuorumFor(method: String): CallQuorum { + return when { + Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum() + Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2) + Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0) + Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(jacksonRpcConverter) + else -> AlwaysQuorum() + } + } + + override fun isAllowed(method: String): Boolean { + return Collections.binarySearch(allowedMethods, method) >= 0 + } + + override fun getSupportedMethods(): Set { + return allowedMethods.toSortedSet() + } + + override fun isHardcoded(method: String): Boolean { + return Collections.binarySearch(hardcodedMethods, method) >= 0; + } + + override fun executeHardcoded(method: String): Any { + return when (method) { + "getconnectioncount" -> 42 + "getnetworkinfo" -> mapOf( + "version" to 700000, + "subversion" to "/EmeraldDshackle:v0.7/" + ) + else -> throw RpcException(-32601, "Method not found") + } + } } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy index 78e784bc..d63a23cb 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy @@ -19,7 +19,7 @@ class BitcoinApiSpec extends Specification { mockServer = ClientAndServer.startClientAndServer(18332); api = new BitcoinApi( new BitcoinRpcClient("localhost:18332", null), - TestingCommons.objectMapper() + TestingCommons.objectMapper(), new DefaultBitcoinMethods(TestingCommons.objectMapper()) ) }