problem: doesn't connect to Bitcoin networks over gRPC
This commit is contained in:
@@ -23,5 +23,6 @@ class Defaults {
|
|||||||
companion object {
|
companion object {
|
||||||
val timeout: Duration = Duration.ofSeconds(60)
|
val timeout: Duration = Duration.ofSeconds(60)
|
||||||
val timeoutInternal: Duration = timeout.dividedBy(4)
|
val timeoutInternal: Duration = timeout.dividedBy(4)
|
||||||
|
val retryConnection: Duration = Duration.ofSeconds(10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ class BlockContainer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun from(raw: ByteArray): BlockContainer {
|
fun fromEthereumJson(raw: ByteArray): BlockContainer {
|
||||||
val block = Global.objectMapper.readValue(raw, BlockJson::class.java)
|
val block = Global.objectMapper.readValue(raw, BlockJson::class.java)
|
||||||
return from(block, raw)
|
return from(block, raw)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,16 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.startup
|
package io.emeraldpay.dshackle.startup
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
import io.emeraldpay.dshackle.Global
|
|
||||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
|
import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumRpcUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumRpcUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory
|
||||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcHttpClient
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcHttpClient
|
||||||
@@ -146,7 +143,7 @@ open class ConfiguredUpstreams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val methods = buildMethods(config, chain)
|
val methods = buildMethods(config, chain)
|
||||||
val upstream = BitcoinUpstream(config.id
|
val upstream = BitcoinRpcUpstream(config.id
|
||||||
?: "bitcoin-${seq.getAndIncrement()}", chain, directApi,
|
?: "bitcoin-${seq.getAndIncrement()}", chain, directApi,
|
||||||
options, config.role,
|
options, config.role,
|
||||||
QuorumForLabels.QuorumItem(1, config.labels),
|
QuorumForLabels.QuorumItem(1, config.labels),
|
||||||
|
|||||||
@@ -58,6 +58,20 @@ class QuorumForLabels() {
|
|||||||
return Collections.unmodifiableList(nodes)
|
return Collections.unmodifiableList(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is QuorumForLabels) return false
|
||||||
|
|
||||||
|
if (nodes != other.nodes) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return nodes.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Details for a single element (upstream, node or aggregation)
|
* Details for a single element (upstream, node or aggregation)
|
||||||
*/
|
*/
|
||||||
@@ -67,6 +81,24 @@ class QuorumForLabels() {
|
|||||||
return QuorumItem(0, UpstreamsConfig.Labels())
|
return QuorumItem(0, UpstreamsConfig.Labels())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is QuorumItem) return false
|
||||||
|
|
||||||
|
if (quorum != other.quorum) return false
|
||||||
|
if (labels != other.labels) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = quorum
|
||||||
|
result = 31 * result + labels.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,14 +16,12 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.Global
|
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||||
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
|
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
@@ -46,6 +47,14 @@ abstract class DefaultUpstream(
|
|||||||
return getStatus() == UpstreamAvailability.OK
|
return getStatus() == UpstreamAvailability.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onStatus(value: BlockchainOuterClass.ChainStatus) {
|
||||||
|
val available = value.availability
|
||||||
|
val quorum = value.quorum
|
||||||
|
setStatus(
|
||||||
|
if (available != null) UpstreamAvailability.fromGrpc(available.number) else UpstreamAvailability.UNAVAILABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getStatus(): UpstreamAvailability {
|
override fun getStatus(): UpstreamAvailability {
|
||||||
return status.get().status
|
return status.get().status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* 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.bitcoin
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.context.Lifecycle
|
||||||
|
import reactor.core.Disposable
|
||||||
|
|
||||||
|
open class BitcoinRpcUpstream(
|
||||||
|
id: String,
|
||||||
|
chain: Chain,
|
||||||
|
private val directApi: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||||
|
options: UpstreamsConfig.Options,
|
||||||
|
role: UpstreamsConfig.UpstreamRole,
|
||||||
|
node: QuorumForLabels.QuorumItem,
|
||||||
|
callMethods: CallMethods
|
||||||
|
) : BitcoinUpstream(id, chain, options, role, callMethods, node), Lifecycle {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BitcoinRpcUpstream::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val head: Head = createHead()
|
||||||
|
private var validatorSubscription: Disposable? = null
|
||||||
|
|
||||||
|
private fun createHead(): Head {
|
||||||
|
return BitcoinRpcHead(
|
||||||
|
directApi,
|
||||||
|
ExtractBlock()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHead(): Head {
|
||||||
|
return head
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
return directApi
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||||
|
return listOf(UpstreamsConfig.Labels())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||||
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
|
}
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean {
|
||||||
|
var runningAny = validatorSubscription != null
|
||||||
|
if (head is Lifecycle) {
|
||||||
|
runningAny = runningAny || head.isRunning
|
||||||
|
}
|
||||||
|
return runningAny
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
log.info("Configured for ${chain.chainName}")
|
||||||
|
if (head is Lifecycle) {
|
||||||
|
if (!head.isRunning) {
|
||||||
|
head.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validatorSubscription?.dispose()
|
||||||
|
|
||||||
|
if (getOptions().disableValidation != null && getOptions().disableValidation!!) {
|
||||||
|
this.setLag(0)
|
||||||
|
this.setStatus(UpstreamAvailability.OK)
|
||||||
|
} else {
|
||||||
|
val validator = BitcoinUpstreamValidator(directApi, getOptions())
|
||||||
|
validatorSubscription = validator.start()
|
||||||
|
.subscribe(this::setStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
if (head is Lifecycle) {
|
||||||
|
head.stop()
|
||||||
|
}
|
||||||
|
validatorSubscription?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,97 +15,30 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
|
||||||
import reactor.core.Disposable
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
|
|
||||||
open class BitcoinUpstream(
|
abstract class BitcoinUpstream(
|
||||||
id: String,
|
id: String,
|
||||||
val chain: Chain,
|
val chain: Chain,
|
||||||
private val directApi: Reader<JsonRpcRequest, JsonRpcResponse>,
|
|
||||||
options: UpstreamsConfig.Options,
|
options: UpstreamsConfig.Options,
|
||||||
role: UpstreamsConfig.UpstreamRole,
|
role: UpstreamsConfig.UpstreamRole,
|
||||||
node: QuorumForLabels.QuorumItem,
|
callMethods: CallMethods,
|
||||||
callMethods: CallMethods
|
node: QuorumForLabels.QuorumItem
|
||||||
) : DefaultUpstream(id, options, role, callMethods, node), Lifecycle {
|
) : DefaultUpstream(id, options, role, callMethods, node) {
|
||||||
|
|
||||||
|
constructor(id: String,
|
||||||
|
chain: Chain,
|
||||||
|
options: UpstreamsConfig.Options,
|
||||||
|
role: UpstreamsConfig.UpstreamRole) : this(id, chain, options, role, DefaultBitcoinMethods(), QuorumForLabels.QuorumItem.empty())
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
|
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val head: Head = createHead()
|
|
||||||
private var validatorSubscription: Disposable? = null
|
|
||||||
|
|
||||||
private fun createHead(): Head {
|
|
||||||
return BitcoinRpcHead(
|
|
||||||
directApi,
|
|
||||||
ExtractBlock()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getHead(): Head {
|
|
||||||
return head
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
|
||||||
return directApi
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
|
||||||
return listOf(UpstreamsConfig.Labels())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
|
||||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
|
||||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
|
||||||
}
|
|
||||||
return this as T
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
|
||||||
var runningAny = validatorSubscription != null
|
|
||||||
if (head is Lifecycle) {
|
|
||||||
runningAny = runningAny || head.isRunning
|
|
||||||
}
|
|
||||||
runningAny = runningAny
|
|
||||||
return runningAny
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
|
||||||
log.info("Configured for ${chain.chainName}")
|
|
||||||
if (head is Lifecycle) {
|
|
||||||
if (!head.isRunning) {
|
|
||||||
head.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validatorSubscription?.dispose()
|
|
||||||
|
|
||||||
if (getOptions().disableValidation != null && getOptions().disableValidation!!) {
|
|
||||||
this.setLag(0)
|
|
||||||
this.setStatus(UpstreamAvailability.OK)
|
|
||||||
} else {
|
|
||||||
val validator = BitcoinUpstreamValidator(directApi, getOptions())
|
|
||||||
validatorSubscription = validator.start()
|
|
||||||
.subscribe(this::setStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
|
||||||
if (head is Lifecycle) {
|
|
||||||
head.stop()
|
|
||||||
}
|
|
||||||
validatorSubscription?.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,14 +16,12 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.infinitape.etherjar.hex.HexQuantity
|
import io.infinitape.etherjar.hex.HexQuantity
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||||
@@ -71,7 +69,7 @@ class EthereumRpcHead(
|
|||||||
.timeout(Defaults.timeout, Mono.error(Exception("Block data not received")))
|
.timeout(Defaults.timeout, Mono.error(Exception("Block data not received")))
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
BlockContainer.from(it.getResult())
|
BlockContainer.fromEthereumJson(it.getResult())
|
||||||
}
|
}
|
||||||
.onErrorContinue { err, _ ->
|
.onErrorContinue { err, _ ->
|
||||||
log.debug("RPC error ${err.message}")
|
log.debug("RPC error ${err.message}")
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class EthereumWsFactory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flatMap(JsonRpcResponse::requireResult)
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
.map { BlockContainer.from(it) }
|
.map { BlockContainer.fromEthereumJson(it) }
|
||||||
}.repeatWhenEmpty { n ->
|
}.repeatWhenEmpty { n ->
|
||||||
Repeat.times<Any>(5)
|
Repeat.times<Any>(5)
|
||||||
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
|
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* 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.grpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import org.reactivestreams.Publisher
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.context.Lifecycle
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
class BitcoinGrpcUpstream(
|
||||||
|
private val parentId: String,
|
||||||
|
chain: Chain,
|
||||||
|
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
|
private val client: JsonRpcGrpcClient
|
||||||
|
) : BitcoinUpstream(
|
||||||
|
"$parentId/${chain.chainCode}",
|
||||||
|
chain,
|
||||||
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
|
UpstreamsConfig.UpstreamRole.STANDARD
|
||||||
|
), GrpcUpstream, Lifecycle {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BitcoinGrpcUpstream::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val extractBlock = ExtractBlock()
|
||||||
|
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
|
||||||
|
private val blockConverter: Function<BlockchainOuterClass.ChainHead, BlockContainer> = Function { value ->
|
||||||
|
val block = BlockContainer(
|
||||||
|
value.height,
|
||||||
|
BlockId.from(value.blockId),
|
||||||
|
BigInteger(1, value.weight.toByteArray()),
|
||||||
|
Instant.ofEpochMilli(value.timestamp),
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
block
|
||||||
|
}
|
||||||
|
private val reloadBlock: Function<BlockContainer, Publisher<BlockContainer>> = Function { existingBlock ->
|
||||||
|
// head comes without transaction data
|
||||||
|
// need to download transactions for the block
|
||||||
|
defaultReader.read(JsonRpcRequest("getblock", listOf(existingBlock.hash.toHex())))
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map(extractBlock::extract)
|
||||||
|
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
|
||||||
|
.doOnError { t ->
|
||||||
|
setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||||
|
val msg = "Failed to download block data for chain $chain on $parentId"
|
||||||
|
if (t is RpcException || t is TimeoutException) {
|
||||||
|
log.warn("$msg. Message: ${t.message}")
|
||||||
|
} else {
|
||||||
|
log.error(msg, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val upstreamStatus = GrpcUpstreamStatus()
|
||||||
|
private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock)
|
||||||
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
|
override fun getHead(): Head {
|
||||||
|
return grpcHead
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
return defaultReader
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||||
|
return upstreamStatus.getLabels()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||||
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
|
}
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean {
|
||||||
|
return grpcHead.isRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
grpcHead.start(remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
grpcHead.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(conf: BlockchainOuterClass.DescribeChain) {
|
||||||
|
upstreamStatus.update(conf)
|
||||||
|
conf.status?.let { status -> onStatus(status) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,9 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.grpc
|
package io.emeraldpay.dshackle.upstream.grpc
|
||||||
|
|
||||||
import com.salesforce.reactorgrpc.GrpcRetry
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
@@ -28,7 +26,6 @@ import io.emeraldpay.dshackle.reader.Reader
|
|||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.*
|
import io.emeraldpay.dshackle.upstream.ethereum.*
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
@@ -36,167 +33,107 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
|||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.rpc.*
|
import io.infinitape.etherjar.rpc.*
|
||||||
|
import org.reactivestreams.Publisher
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
|
||||||
import reactor.core.publisher.Flux
|
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.core.publisher.toMono
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
open class EthereumGrpcUpstream(
|
open class EthereumGrpcUpstream(
|
||||||
private val parentId: String,
|
private val parentId: String,
|
||||||
private val chain: Chain,
|
private val chain: Chain,
|
||||||
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val client: JsonRpcGrpcClient
|
private val client: JsonRpcGrpcClient
|
||||||
) : EthereumUpstream(
|
) : EthereumUpstream(
|
||||||
"$parentId/${chain.chainCode}",
|
"$parentId/${chain.chainCode}",
|
||||||
UpstreamsConfig.Options.getDefaults(),
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
UpstreamsConfig.UpstreamRole.STANDARD,
|
UpstreamsConfig.UpstreamRole.STANDARD,
|
||||||
null, null
|
null, null
|
||||||
), Lifecycle {
|
), GrpcUpstream, Lifecycle {
|
||||||
|
|
||||||
|
private val blockConverter: Function<BlockchainOuterClass.ChainHead, BlockContainer> = Function { value ->
|
||||||
|
val block = BlockContainer(
|
||||||
|
value.height,
|
||||||
|
BlockId.from(BlockHash.from("0x" + value.blockId)),
|
||||||
|
BigInteger(1, value.weight.toByteArray()),
|
||||||
|
Instant.ofEpochMilli(value.timestamp),
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
private val reloadBlock: Function<BlockContainer, Publisher<BlockContainer>> = Function { existingBlock ->
|
||||||
|
// head comes without transaction data
|
||||||
|
// need to download transactions for the block
|
||||||
|
defaultReader.read(JsonRpcRequest("eth_getBlockByHash", listOf(existingBlock.hash.toHexWithPrefix(), false)))
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map {
|
||||||
|
BlockContainer.fromEthereumJson(it)
|
||||||
|
}
|
||||||
|
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
|
||||||
|
.doOnError { t ->
|
||||||
|
setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||||
|
val msg = "Failed to download block data for chain $chain on $parentId"
|
||||||
|
if (t is RpcException || t is TimeoutException) {
|
||||||
|
log.warn("$msg. Message: ${t.message}")
|
||||||
|
} else {
|
||||||
|
log.error(msg, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
|
|
||||||
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
|
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
|
||||||
|
private val upstreamStatus = GrpcUpstreamStatus()
|
||||||
private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels())
|
private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock)
|
||||||
private val head = DefaultEthereumHead()
|
|
||||||
private var targets: CallMethods? = null
|
|
||||||
private var headSubscription: Disposable? = null
|
|
||||||
|
|
||||||
var timeout = Defaults.timeout
|
|
||||||
|
|
||||||
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
|
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
|
||||||
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (this.isRunning) return
|
grpcHead.start(remote)
|
||||||
val chainRef = Common.Chain.newBuilder()
|
|
||||||
.setTypeValue(chain.id)
|
|
||||||
.build()
|
|
||||||
.toMono()
|
|
||||||
|
|
||||||
val retry: Function<Flux<BlockchainOuterClass.ChainHead>, Flux<BlockchainOuterClass.ChainHead>> = Function {
|
|
||||||
setStatus(UpstreamAvailability.UNAVAILABLE)
|
|
||||||
blockchainStub.subscribeHead(chainRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
val flux = blockchainStub.subscribeHead(chainRef)
|
|
||||||
.compose(GrpcRetry.ManyToMany.retryAfter(retry, Duration.ofSeconds(5)))
|
|
||||||
observeHead(flux)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
return headSubscription != null
|
return grpcHead.isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
headSubscription?.dispose()
|
grpcHead.stop()
|
||||||
headSubscription = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun update(conf: BlockchainOuterClass.DescribeChain) {
|
||||||
internal fun observeHead(flux: Flux<BlockchainOuterClass.ChainHead>) {
|
upstreamStatus.update(conf)
|
||||||
val base = flux.map { value ->
|
|
||||||
val block = BlockContainer(
|
|
||||||
value.height,
|
|
||||||
BlockId.from(BlockHash.from("0x" + value.blockId)),
|
|
||||||
BigInteger(1, value.weight.toByteArray()),
|
|
||||||
Instant.ofEpochMilli(value.timestamp),
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
block
|
|
||||||
}.distinctUntilChanged {
|
|
||||||
it.hash
|
|
||||||
}.filter { block ->
|
|
||||||
val curr = head.getCurrent()
|
|
||||||
curr == null || curr.difficulty < block.difficulty
|
|
||||||
}.flatMap {
|
|
||||||
defaultReader.read(JsonRpcRequest("eth_getBlockByHash", listOf(it.hash.toHexWithPrefix(), false)))
|
|
||||||
.flatMap(JsonRpcResponse::requireResult)
|
|
||||||
.map {
|
|
||||||
BlockContainer.from(it)
|
|
||||||
}
|
|
||||||
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
|
|
||||||
.doOnError { t ->
|
|
||||||
setStatus(UpstreamAvailability.UNAVAILABLE)
|
|
||||||
val msg = "Failed to download block data for chain $chain on $parentId"
|
|
||||||
if (t is RpcException || t is TimeoutException) {
|
|
||||||
log.warn("$msg. Message: ${t.message}")
|
|
||||||
} else {
|
|
||||||
log.error(msg, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onErrorContinue { err, _ ->
|
|
||||||
log.error("Head subscription error. ${err.javaClass.name}:${err.message}", err)
|
|
||||||
}.doOnNext {
|
|
||||||
setStatus(UpstreamAvailability.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
headSubscription = head.follow(base)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init(conf: BlockchainOuterClass.DescribeChain) {
|
|
||||||
targets = DirectCallMethods(conf.supportedMethodsList.toSet())
|
|
||||||
val nodes = QuorumForLabels()
|
|
||||||
val allLabels = ArrayList<UpstreamsConfig.Labels>()
|
|
||||||
conf.nodesList.forEach { remoteNode ->
|
|
||||||
val node = QuorumForLabels.QuorumItem(remoteNode.quorum,
|
|
||||||
remoteNode.labelsList.let { provided ->
|
|
||||||
val labels = UpstreamsConfig.Labels()
|
|
||||||
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) }
|
conf.status?.let { status -> onStatus(status) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onStatus(value: BlockchainOuterClass.ChainStatus) {
|
|
||||||
val available = value.availability
|
|
||||||
val quorum = value.quorum
|
|
||||||
setStatus(
|
|
||||||
if (available != null) UpstreamAvailability.fromGrpc(available.number) else UpstreamAvailability.UNAVAILABLE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getQuorumByLabel(): QuorumForLabels {
|
override fun getQuorumByLabel(): QuorumForLabels {
|
||||||
return nodes.get()
|
return upstreamStatus.getNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||||
return allLabels
|
return upstreamStatus.getLabels()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMethods(): CallMethods {
|
override fun getMethods(): CallMethods {
|
||||||
return targets ?: throw IllegalStateException("Upstream is not initialized yet")
|
return upstreamStatus.getCallMethods()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
override fun isAvailable(): Boolean {
|
||||||
return super.isAvailable() && head.getCurrent() != null && nodes.get().getAll().any {
|
return super.isAvailable() && grpcHead.getCurrent() != null && getQuorumByLabel().getAll().any {
|
||||||
it.quorum > 0
|
it.quorum > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHead(): Head {
|
override fun getHead(): Head {
|
||||||
return head
|
return grpcHead
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
|||||||
121
src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcHead.kt
Normal file
121
src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcHead.kt
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* 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.grpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.upstream.AbstractHead
|
||||||
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import org.reactivestreams.Publisher
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.context.Lifecycle
|
||||||
|
import reactor.core.Disposable
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.util.retry.Retry
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
class GrpcHead(
|
||||||
|
private val chain: Chain,
|
||||||
|
private val parent: DefaultUpstream,
|
||||||
|
/**
|
||||||
|
* Converted from remote head details to the block container, which could be partial at this point
|
||||||
|
*/
|
||||||
|
private val converter: Function<BlockchainOuterClass.ChainHead, BlockContainer>,
|
||||||
|
/**
|
||||||
|
* Populate block data with all missing details, of any
|
||||||
|
*/
|
||||||
|
private val enhancer: Function<BlockContainer, Publisher<BlockContainer>>?
|
||||||
|
) : AbstractHead(), Lifecycle {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(GrpcHead::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var headSubscription: Disposable? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate a new head subscription with connection to the remote
|
||||||
|
*/
|
||||||
|
fun start(remote: ReactorBlockchainGrpc.ReactorBlockchainStub) {
|
||||||
|
if (this.isRunning) {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
val source = Flux.concat(
|
||||||
|
// first connect immediately
|
||||||
|
Flux.just(remote),
|
||||||
|
// following requests do with delay, give it a time to recover
|
||||||
|
Flux.just(remote).repeat().delayElements(Defaults.retryConnection)
|
||||||
|
).flatMap(this::subscribeHead)
|
||||||
|
|
||||||
|
start(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun subscribeHead(client: ReactorBlockchainGrpc.ReactorBlockchainStub): Publisher<BlockchainOuterClass.ChainHead> {
|
||||||
|
val chainRef = Common.Chain.newBuilder()
|
||||||
|
.setTypeValue(chain.id)
|
||||||
|
.build()
|
||||||
|
return client.subscribeHead(chainRef)
|
||||||
|
// simple retry on failure, if eventually failed then it supposed to resubscribe later from outer method
|
||||||
|
.retryWhen(Retry.backoff(4, Duration.ofSeconds(1)))
|
||||||
|
.onErrorContinue { err, _ ->
|
||||||
|
log.warn("Disconnected $chain from ${parent.getId()}: ${err.message}")
|
||||||
|
parent.setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||||
|
Mono.empty<BlockchainOuterClass.ChainHead>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate a new head from provided source of head details
|
||||||
|
*/
|
||||||
|
fun start(source: Flux<BlockchainOuterClass.ChainHead>) {
|
||||||
|
var blocks = source.map(converter)
|
||||||
|
.distinctUntilChanged {
|
||||||
|
it.hash
|
||||||
|
}.filter { block ->
|
||||||
|
val curr = this.getCurrent()
|
||||||
|
curr == null || curr.difficulty < block.difficulty
|
||||||
|
}
|
||||||
|
if (enhancer != null) {
|
||||||
|
blocks = blocks.flatMap(enhancer)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks = blocks.onErrorContinue { err, _ ->
|
||||||
|
log.error("Head subscription error. ${err.javaClass.name}:${err.message}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headSubscription = super.follow(blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean {
|
||||||
|
return !(headSubscription?.isDisposed ?: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
log.error("Use start with provides source")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
headSubscription?.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* 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.grpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
|
||||||
|
interface GrpcUpstream {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the configuration of the upstream with the new data.
|
||||||
|
* Called on the first creation, and each time a new state received from upstream
|
||||||
|
*/
|
||||||
|
fun update(conf: BlockchainOuterClass.DescribeChain)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* 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.grpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class GrpcUpstreamStatus {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(GrpcUpstreamStatus::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val allLabels: AtomicReference<Collection<UpstreamsConfig.Labels>> = AtomicReference(emptyList())
|
||||||
|
private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels())
|
||||||
|
private var targets: CallMethods? = null
|
||||||
|
|
||||||
|
fun update(conf: BlockchainOuterClass.DescribeChain) {
|
||||||
|
val updateLabels = ArrayList<UpstreamsConfig.Labels>()
|
||||||
|
val updateNodes = QuorumForLabels()
|
||||||
|
|
||||||
|
conf.nodesList.forEach { remoteNode ->
|
||||||
|
val node = QuorumForLabels.QuorumItem(remoteNode.quorum,
|
||||||
|
remoteNode.labelsList.let { provided ->
|
||||||
|
val labels = UpstreamsConfig.Labels()
|
||||||
|
provided.forEach {
|
||||||
|
labels[it.name] = it.value
|
||||||
|
}
|
||||||
|
updateLabels.add(labels)
|
||||||
|
labels
|
||||||
|
}
|
||||||
|
)
|
||||||
|
updateNodes.add(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodes.set(updateNodes)
|
||||||
|
this.allLabels.set(Collections.unmodifiableCollection(updateLabels))
|
||||||
|
this.targets = DirectCallMethods(conf.supportedMethodsList.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||||
|
return allLabels.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNodes(): QuorumForLabels {
|
||||||
|
return nodes.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCallMethods(): CallMethods {
|
||||||
|
return targets ?: throw IllegalStateException("Upstream is not initialized yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.grpc
|
package io.emeraldpay.dshackle.upstream.grpc
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
@@ -25,6 +24,7 @@ import io.emeraldpay.dshackle.FileResolver
|
|||||||
import io.emeraldpay.dshackle.config.AuthConfig
|
import io.emeraldpay.dshackle.config.AuthConfig
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||||
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.ManagedChannelBuilder
|
import io.grpc.ManagedChannelBuilder
|
||||||
@@ -54,7 +54,7 @@ class GrpcUpstreams(
|
|||||||
var timeout = Defaults.timeout
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
||||||
private val known = HashMap<Chain, EthereumGrpcUpstream>()
|
private val known = HashMap<Chain, DefaultUpstream>()
|
||||||
private val lock = ReentrantLock()
|
private val lock = ReentrantLock()
|
||||||
|
|
||||||
fun start(): Flux<UpstreamChange> {
|
fun start(): Flux<UpstreamChange> {
|
||||||
@@ -115,7 +115,7 @@ class GrpcUpstreams(
|
|||||||
try {
|
try {
|
||||||
val chain = Chain.byId(chainDetails.chain.number)
|
val chain = Chain.byId(chainDetails.chain.number)
|
||||||
val up = getOrCreate(chain)
|
val up = getOrCreate(chain)
|
||||||
(up.upstream as EthereumGrpcUpstream).init(chainDetails)
|
(up.upstream as GrpcUpstream).update(chainDetails)
|
||||||
up
|
up
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
log.warn("Skip unsupported upstream ${chainDetails.chain} on $id: ${e.message}")
|
log.warn("Skip unsupported upstream ${chainDetails.chain} on $id: ${e.message}")
|
||||||
@@ -157,9 +157,17 @@ class GrpcUpstreams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getOrCreate(chain: Chain): UpstreamChange {
|
fun getOrCreate(chain: Chain): UpstreamChange {
|
||||||
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
|
val blockchainType = BlockchainType.fromBlockchain(chain)
|
||||||
|
if (blockchainType == BlockchainType.ETHEREUM) {
|
||||||
|
return getOrCreateEthereum(chain)
|
||||||
|
} else if (blockchainType == BlockchainType.BITCOIN) {
|
||||||
|
return getOrCreateBitcoin(chain)
|
||||||
|
} else {
|
||||||
throw IllegalArgumentException("Unsupported blockchain: $chain")
|
throw IllegalArgumentException("Unsupported blockchain: $chain")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrCreateEthereum(chain: Chain): UpstreamChange {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
val current = known[chain]
|
val current = known[chain]
|
||||||
return if (current == null) {
|
return if (current == null) {
|
||||||
@@ -175,7 +183,23 @@ class GrpcUpstreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(chain: Chain): EthereumGrpcUpstream {
|
fun getOrCreateBitcoin(chain: Chain): UpstreamChange {
|
||||||
|
lock.withLock {
|
||||||
|
val current = known[chain]
|
||||||
|
return if (current == null) {
|
||||||
|
val rpcClient = JsonRpcGrpcClient(client!!, chain)
|
||||||
|
val created = BitcoinGrpcUpstream(id, chain, client!!, rpcClient)
|
||||||
|
created.timeout = this.timeout
|
||||||
|
known[chain] = created
|
||||||
|
created.start()
|
||||||
|
UpstreamChange(chain, created, UpstreamChange.ChangeType.ADDED)
|
||||||
|
} else {
|
||||||
|
UpstreamChange(chain, current, UpstreamChange.ChangeType.REVALIDATED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(chain: Chain): DefaultUpstream {
|
||||||
return known[chain]!!
|
return known[chain]!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ import io.emeraldpay.dshackle.BlockchainType
|
|||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.upstream.Multistream
|
import io.emeraldpay.dshackle.upstream.Multistream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
@@ -55,8 +55,8 @@ class MultistreamHolderMock implements MultistreamHolder {
|
|||||||
} else if (BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN) {
|
} else if (BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN) {
|
||||||
if (up instanceof BitcoinMultistream) {
|
if (up instanceof BitcoinMultistream) {
|
||||||
upstreams[chain] = up
|
upstreams[chain] = up
|
||||||
} else if (up instanceof BitcoinUpstream) {
|
} else if (up instanceof BitcoinRpcUpstream) {
|
||||||
upstreams[chain] = new BitcoinMultistream(chain, [up as BitcoinUpstream], Caches.default())
|
upstreams[chain] = new BitcoinMultistream(chain, [up as BitcoinRpcUpstream], Caches.default())
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
|
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,12 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
|||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.time.temporal.TemporalUnit
|
||||||
|
|
||||||
class TestingCommons {
|
class TestingCommons {
|
||||||
|
|
||||||
@@ -86,9 +90,27 @@ class TestingCommons {
|
|||||||
setNumber(height)
|
setNumber(height)
|
||||||
setHash(BlockHash.from("0xc4b01774e426325b50f0c709753ec7cf1f1774439d587dfb91f2a4eeb8179cde"))
|
setHash(BlockHash.from("0xc4b01774e426325b50f0c709753ec7cf1f1774439d587dfb91f2a4eeb8179cde"))
|
||||||
setTotalDifficulty(BigInteger.ONE)
|
setTotalDifficulty(BigInteger.ONE)
|
||||||
setTimestamp(Instant.now())
|
setTimestamp(predictableTimestamp(height, 14))
|
||||||
}
|
}
|
||||||
return BlockContainer.from(block)
|
return BlockContainer.from(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static BlockContainer blockForBitcoin(Long height) {
|
||||||
|
return new BlockContainer(
|
||||||
|
height,
|
||||||
|
BlockId.from(StringUtils.leftPad(height.toString(), 64, "0")),
|
||||||
|
BigInteger.valueOf(height),
|
||||||
|
predictableTimestamp(height, 60),
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Instant predictableTimestamp(Long x, int stepSeconds) {
|
||||||
|
//start from 1 Jan 2020
|
||||||
|
Instant.ofEpochSecond(1577876400)
|
||||||
|
.plusSeconds(x * stepSeconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ import io.emeraldpay.dshackle.data.BlockContainer
|
|||||||
import io.emeraldpay.dshackle.data.BlockId
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
import io.emeraldpay.dshackle.data.TxContainer
|
import io.emeraldpay.dshackle.data.TxContainer
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.test.ReaderMock
|
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
@@ -258,7 +256,7 @@ class EthereumFullBlocksReaderSpec extends Specification {
|
|||||||
"extraField2": "extraValue2"
|
"extraField2": "extraValue2"
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
blocks.add(BlockContainer.from(blockJson.bytes))
|
blocks.add(BlockContainer.fromEthereumJson(blockJson.bytes))
|
||||||
|
|
||||||
def tx1 = '''
|
def tx1 = '''
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ class EthereumGrpcUpstreamSpec extends Specification {
|
|||||||
})
|
})
|
||||||
def upstream = new EthereumGrpcUpstream("test", chain, client, new JsonRpcGrpcClient(client, chain))
|
def upstream = new EthereumGrpcUpstream("test", chain, client, new JsonRpcGrpcClient(client, chain))
|
||||||
upstream.setLag(0)
|
upstream.setLag(0)
|
||||||
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
||||||
.addAllSupportedMethods(["eth_getBlockByHash"])
|
.addAllSupportedMethods(["eth_getBlockByHash"])
|
||||||
.build())
|
.build())
|
||||||
when:
|
when:
|
||||||
@@ -132,7 +133,8 @@ class EthereumGrpcUpstreamSpec extends Specification {
|
|||||||
})
|
})
|
||||||
def upstream = new EthereumGrpcUpstream("test", Chain.ETHEREUM, client, new JsonRpcGrpcClient(client, Chain.ETHEREUM))
|
def upstream = new EthereumGrpcUpstream("test", Chain.ETHEREUM, client, new JsonRpcGrpcClient(client, Chain.ETHEREUM))
|
||||||
upstream.setLag(0)
|
upstream.setLag(0)
|
||||||
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
||||||
.addAllSupportedMethods(["eth_getBlockByHash"])
|
.addAllSupportedMethods(["eth_getBlockByHash"])
|
||||||
.build())
|
.build())
|
||||||
when:
|
when:
|
||||||
@@ -193,7 +195,8 @@ class EthereumGrpcUpstreamSpec extends Specification {
|
|||||||
})
|
})
|
||||||
def upstream = new EthereumGrpcUpstream("test", chain, client, new JsonRpcGrpcClient(client, chain))
|
def upstream = new EthereumGrpcUpstream("test", chain, client, new JsonRpcGrpcClient(client, chain))
|
||||||
upstream.setLag(0)
|
upstream.setLag(0)
|
||||||
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.update(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
|
||||||
.addAllSupportedMethods(["eth_getBlockByHash"])
|
.addAllSupportedMethods(["eth_getBlockByHash"])
|
||||||
.build())
|
.build())
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* 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.grpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.test.MockGrpcServer
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import reactor.test.StepVerifier
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
class GrpcHeadSpec extends Specification {
|
||||||
|
|
||||||
|
MockGrpcServer mockServer = new MockGrpcServer()
|
||||||
|
|
||||||
|
def "Subscribe to remote"() {
|
||||||
|
setup:
|
||||||
|
def client = mockServer.clientForServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
@Override
|
||||||
|
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
|
||||||
|
if (request.type.number != Chain.BITCOIN.id) {
|
||||||
|
responseObserver.onError(new IllegalStateException("Unsupported chain"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
[10, 11, 12, 14].forEach { height ->
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setChain(request.type)
|
||||||
|
.setHeight(height)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
Thread.sleep(100)
|
||||||
|
}
|
||||||
|
responseObserver.onCompleted()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def convert = { BlockchainOuterClass.ChainHead head ->
|
||||||
|
TestingCommons.blockForBitcoin(head.height)
|
||||||
|
}
|
||||||
|
def head = new GrpcHead(
|
||||||
|
Chain.BITCOIN,
|
||||||
|
Stub(DefaultUpstream),
|
||||||
|
convert, null
|
||||||
|
)
|
||||||
|
when:
|
||||||
|
def act = head.getFlux()
|
||||||
|
.take(3)
|
||||||
|
head.start(client)
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext(TestingCommons.blockForBitcoin(10))
|
||||||
|
.expectNext(TestingCommons.blockForBitcoin(11))
|
||||||
|
.expectNext(TestingCommons.blockForBitcoin(12))
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Reconnect to remote on error"() {
|
||||||
|
setup:
|
||||||
|
int phase = 0
|
||||||
|
def client = mockServer.clientForServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
@Override
|
||||||
|
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
|
||||||
|
if (request.type.number != Chain.BITCOIN.id) {
|
||||||
|
responseObserver.onError(new IllegalStateException("Unsupported chain"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
phase++
|
||||||
|
if (phase == 1) {
|
||||||
|
responseObserver.onError(new RuntimeException("Phase 1 error"))
|
||||||
|
} else if (phase == 2) {
|
||||||
|
[10, 11].forEach { height ->
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setChain(request.type)
|
||||||
|
.setHeight(height)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
Thread.sleep(100)
|
||||||
|
}
|
||||||
|
responseObserver.onError(new RuntimeException("Phase 2 error"))
|
||||||
|
} else if (phase == 3) {
|
||||||
|
[11, 12, 13].forEach { height ->
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setChain(request.type)
|
||||||
|
.setHeight(height)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
Thread.sleep(100)
|
||||||
|
}
|
||||||
|
responseObserver.onCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def convert = { BlockchainOuterClass.ChainHead head ->
|
||||||
|
TestingCommons.blockForBitcoin(head.height)
|
||||||
|
}
|
||||||
|
def head = new GrpcHead(
|
||||||
|
Chain.BITCOIN,
|
||||||
|
Stub(DefaultUpstream),
|
||||||
|
convert, null
|
||||||
|
)
|
||||||
|
when:
|
||||||
|
def act = head.getFlux()
|
||||||
|
.take(3)
|
||||||
|
head.start(client)
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext(TestingCommons.blockForBitcoin(10))
|
||||||
|
.expectNext(TestingCommons.blockForBitcoin(11))
|
||||||
|
.expectNext(TestingCommons.blockForBitcoin(12))
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* 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.grpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class GrpcUpstreamStatusSpec extends Specification {
|
||||||
|
|
||||||
|
def "Updates with new labels"() {
|
||||||
|
setup:
|
||||||
|
def status = new GrpcUpstreamStatus()
|
||||||
|
when:
|
||||||
|
status.update(
|
||||||
|
BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.addNodes(
|
||||||
|
BlockchainOuterClass.NodeDetails.newBuilder()
|
||||||
|
.setQuorum(1)
|
||||||
|
.addLabels(
|
||||||
|
BlockchainOuterClass.Label.newBuilder().setName("test").setValue("foo")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
def act = status.getLabels()
|
||||||
|
then:
|
||||||
|
act.toList() == [
|
||||||
|
UpstreamsConfig.Labels.fromMap([test: "foo"])
|
||||||
|
]
|
||||||
|
|
||||||
|
// replace with new value
|
||||||
|
when:
|
||||||
|
status.update(
|
||||||
|
BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.addNodes(
|
||||||
|
BlockchainOuterClass.NodeDetails.newBuilder()
|
||||||
|
.setQuorum(1)
|
||||||
|
.addLabels(
|
||||||
|
BlockchainOuterClass.Label.newBuilder().setName("test").setValue("bar")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
act = status.getLabels()
|
||||||
|
then:
|
||||||
|
act.toList() == [
|
||||||
|
UpstreamsConfig.Labels.fromMap([test: "bar"])
|
||||||
|
]
|
||||||
|
|
||||||
|
// more values
|
||||||
|
when:
|
||||||
|
status.update(
|
||||||
|
BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.addNodes(
|
||||||
|
BlockchainOuterClass.NodeDetails.newBuilder()
|
||||||
|
.setQuorum(1)
|
||||||
|
.addLabels(
|
||||||
|
BlockchainOuterClass.Label.newBuilder().setName("test1").setValue("bar")
|
||||||
|
)
|
||||||
|
.addLabels(
|
||||||
|
BlockchainOuterClass.Label.newBuilder().setName("test2").setValue("baz")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
act = status.getLabels()
|
||||||
|
then:
|
||||||
|
act.toList() == [
|
||||||
|
UpstreamsConfig.Labels.fromMap([test1: "bar", test2: "baz"])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Updates with new nodes"() {
|
||||||
|
setup:
|
||||||
|
def status = new GrpcUpstreamStatus()
|
||||||
|
when:
|
||||||
|
status.update(
|
||||||
|
BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.addNodes(
|
||||||
|
BlockchainOuterClass.NodeDetails.newBuilder()
|
||||||
|
.setQuorum(1)
|
||||||
|
.addLabels(
|
||||||
|
BlockchainOuterClass.Label.newBuilder().setName("test").setValue("foo")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
def act = status.getNodes()
|
||||||
|
then:
|
||||||
|
act == new QuorumForLabels().tap {
|
||||||
|
it.add(new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap([test: "foo"])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Updates with methods"() {
|
||||||
|
setup:
|
||||||
|
def status = new GrpcUpstreamStatus()
|
||||||
|
when:
|
||||||
|
status.update(
|
||||||
|
BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.addAllSupportedMethods([
|
||||||
|
"test_1",
|
||||||
|
"test_2"
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
def act = status.getCallMethods()
|
||||||
|
then:
|
||||||
|
act.supportedMethods == ["test_1", "test_2"].toSet()
|
||||||
|
act instanceof DirectCallMethods
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user