problem: doesn't connect to Bitcoin networks over gRPC

This commit is contained in:
Igor Artamonov
2020-08-31 21:25:43 -04:00
parent 813f2ae4a2
commit ff3fc95711
22 changed files with 899 additions and 218 deletions

View File

@@ -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)
} }
} }

View File

@@ -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)
} }

View File

@@ -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),

View File

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

View File

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

View File

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

View File

@@ -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()
}
}

View File

@@ -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()
}
} }

View File

@@ -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}")

View File

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

View File

@@ -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) }
}
}

View File

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

View 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()
}
}

View File

@@ -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)
}

View File

@@ -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")
}
}

View File

@@ -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]!!
} }
} }

View File

@@ -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}")
} }

View File

@@ -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)
}
} }

View File

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

View File

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

View File

@@ -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))
}
}

View File

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