solution: refactor upstreams to make them blockchain specific

This commit is contained in:
Igor Artamonov
2020-04-11 22:55:47 -04:00
parent 0ae9d8ec3c
commit 0ab68ea782
49 changed files with 600 additions and 314 deletions

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle
import io.emeraldpay.grpc.Chain
/**
* Type of the blockchain, architecture-wise
*/
enum class BlockchainType {
ETHEREUM,
BITCOIN,
OTHER;
companion object {
@JvmStatic
fun fromBlockchain(blockchain: Chain): BlockchainType {
if (blockchain == Chain.ETHEREUM || blockchain == Chain.ETHEREUM_CLASSIC || blockchain == Chain.TESTNET_KOVAN || blockchain == Chain.TESTNET_MORDEN) {
return ETHEREUM
}
if (blockchain == Chain.BITCOIN || blockchain == Chain.TESTNET_BITCOIN) {
return BITCOIN
}
return OTHER
}
}
}

View File

@@ -16,6 +16,7 @@
package io.emeraldpay.dshackle.config
import io.emeraldpay.dshackle.Defaults
import java.lang.ClassCastException
import java.net.URI
import java.util.*
import kotlin.collections.ArrayList
@@ -72,6 +73,14 @@ class UpstreamsConfig {
var connection: T? = null
val labels = Labels()
var methods: Methods? = null
@Suppress("unchecked")
fun <Z : UpstreamConnection> cast(type: Class<Z>): Upstream<Z> {
if (connection == null || type.isAssignableFrom(connection!!.javaClass)) {
return this as Upstream<Z>
}
throw ClassCastException("Cannot cast ${connection?.javaClass} to $type")
}
}
open class UpstreamConnection

View File

@@ -34,13 +34,13 @@ open class AlwaysQuorum: CallQuorum {
return resolved
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
override fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean {
result = response
resolved = true
return true
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: RpcException, upstream: Upstream<*, *>) {
}
override fun getResult(): ByteArray? {

View File

@@ -42,7 +42,7 @@ open class BroadcastQuorum(
return result
}
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*, *>) {
calls++
if (txid == null && responseValue != null) {
txid = responseValue
@@ -50,7 +50,7 @@ open class BroadcastQuorum(
}
}
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>) {
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
calls++
if (result == null) {

View File

@@ -30,8 +30,8 @@ interface CallQuorum {
fun init(head: Head<BlockJson<TransactionRefJson>>)
fun isResolved(): Boolean
fun record(response: ByteArray, upstream: Upstream): Boolean
fun record(error: RpcException, upstream: Upstream)
fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean
fun record(error: RpcException, upstream: Upstream<*, *>)
fun getResult(): ByteArray?
companion object {
@@ -41,8 +41,8 @@ interface CallQuorum {
}
}
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> {
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { a, b ->
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*, *>>, CallQuorum> {
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*, *>>, CallQuorum> { a, b ->
a.record(b.t1, b.t2)
return@BiFunction a
}

View File

@@ -38,7 +38,7 @@ open class NonEmptyQuorum(
return result != null || tries >= maxTries
}
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*, *>) {
tries++
if (responseValue != null) {
result = response
@@ -49,10 +49,10 @@ open class NonEmptyQuorum(
return result
}
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>) {
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: RpcException, upstream: Upstream<*, *>) {
}
}

View File

@@ -46,7 +46,7 @@ open class NonceQuorum(
}
}
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*, *>) {
val value = responseValue?.let { str ->
HexQuantity.from(str).value.toLong()
}
@@ -65,11 +65,11 @@ open class NonceQuorum(
return result
}
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>) {
errors++
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: RpcException, upstream: Upstream<*, *>) {
errors++
}

View File

@@ -34,7 +34,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return result.get() != null
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
override fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean {
val lagging = upstream.getLag() > maxLag
if (!lagging) {
result.set(response)
@@ -43,7 +43,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return false
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: RpcException, upstream: Upstream<*, *>) {
}

View File

@@ -31,7 +31,7 @@ abstract class ValueAwareQuorum<T>(
return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
override fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean {
try {
val value = extractValue(response, clazz)
recordValue(response, value, upstream)
@@ -43,12 +43,12 @@ abstract class ValueAwareQuorum<T>(
return isResolved();
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: RpcException, upstream: Upstream<*, *>) {
recordError(null, error.rpcMessage, upstream)
}
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream<*, *>)
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream)
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>)
}

View File

@@ -1,44 +0,0 @@
/**
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.reader
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.Commands
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import reactor.core.publisher.Mono
import reactor.retry.Repeat
import java.time.Duration
class BlockApiReader(
val upstream: Upstream
): Reader<BlockHash, BlockJson<TransactionRefJson>> {
override fun read(key: BlockHash): Mono<BlockJson<TransactionRefJson>> {
return Mono.just(key)
.flatMap {
upstream.getApi(Selector.empty)
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(it)) }
}.repeatWhenEmpty { n ->
Repeat.times<Any>(3)
.exponentialBackoff(Duration.ofMillis(100), Duration.ofMillis(500))
.apply(n)
}
}
}

View File

@@ -18,7 +18,8 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.grpc.stub.StreamObserver
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@@ -29,8 +30,8 @@ import reactor.core.publisher.Mono
class BlockchainRpc(
@Autowired private val nativeCall: NativeCall,
@Autowired private val streamHead: StreamHead,
@Autowired private val trackTx: TrackTx,
@Autowired private val trackAddress: TrackAddress,
@Autowired private val trackEthereumTx: TrackEthereumTx,
@Autowired private val trackEthereumAddress: TrackEthereumAddress,
@Autowired private val describe: Describe,
@Autowired private val subscribeStatus: SubscribeStatus
): ReactorBlockchainGrpc.BlockchainImplBase() {
@@ -46,15 +47,15 @@ class BlockchainRpc(
}
override fun subscribeTxStatus(request: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
return trackTx.add(request)
return trackEthereumTx.add(request)
}
override fun subscribeBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return trackAddress.subscribe(request)
return trackEthereumAddress.subscribe(request)
}
override fun getBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return trackAddress.getBalance(request)
return trackEthereumAddress.getBalance(request)
}
override fun describe(request: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {

View File

@@ -20,7 +20,7 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
import io.emeraldpay.dshackle.upstream.grpc.EthereumGrpcUpstream
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@@ -47,7 +47,7 @@ class Describe(
val nodes = QuorumForLabels()
if (up is EthereumUpstream) {
nodes.add(up.node)
} else if (up is GrpcUpstream) {
} else if (up is EthereumGrpcUpstream) {
nodes.add(up.getNodes())
}
nodes.getAll().forEach { node ->

View File

@@ -18,12 +18,16 @@ package io.emeraldpay.dshackle.rpc
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
@@ -84,13 +88,17 @@ open class NativeCall(
if (chain == Chain.UNSPECIFIED) {
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
}
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
}
val upstream = upstreams.getUpstream(chain)
?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain)))
return prepareCall(request, upstream)
return prepareCall(request, upstream as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>)
}
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux<CallContext<RawCallDetails>> {
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>): Flux<CallContext<RawCallDetails>> {
return request.itemsList.toFlux().map {
val method = it.method
val params = it.payload.toStringUtf8()
@@ -197,7 +205,7 @@ open class NativeCall(
}
open class CallContext<T>(val id: Int,
val upstream: AggregatedUpstream,
val upstream: AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>,
val matcher: Selector.Matcher,
val callQuorum: CallQuorum,
val payload: T) {
@@ -205,7 +213,7 @@ open class NativeCall(
return CallContext(id, upstream, matcher, callQuorum, payload)
}
fun getApis(): ApiSource {
fun getApis(): ApiSource<EthereumApi> {
return upstream.getApis(matcher)
}
}

View File

@@ -18,9 +18,9 @@ package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
@@ -44,11 +44,25 @@ class StreamHead(
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
up.getHead()
.getFlux()
.map { asProto(chain, it) }
.map { asProto(chain, it!!) }
.onErrorContinue { t, _ ->
log.warn("Head error: ${t.message}")
}
}
}
fun asProto(chain: Chain, block: BlockJson<TransactionRefJson>): BlockchainOuterClass.ChainHead {
fun asProto(chain: Chain, block: Any): BlockchainOuterClass.ChainHead {
if (BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM) {
if (BlockJson::class.java.isAssignableFrom(block.javaClass)) {
return asEthereumProto(chain, block as BlockJson<TransactionRefJson>)
} else {
throw IllegalArgumentException("Invalid block type: ${block.javaClass}")
}
}
throw IllegalArgumentException("Unsupported blockchain ${chain}")
}
fun asEthereumProto(chain: Chain, block: BlockJson<TransactionRefJson>): BlockchainOuterClass.ChainHead {
return BlockchainOuterClass.ChainHead.newBuilder()
.setChainValue(chain.id)
.setHeight(block.number)

View File

@@ -45,7 +45,7 @@ class SubscribeStatus(
}
}
fun chainStatus(chain: Chain, ups: List<Upstream>): BlockchainOuterClass.ChainStatus {
fun chainStatus(chain: Chain, ups: List<Upstream<*, *>>): BlockchainOuterClass.ChainStatus {
val available = ups.map { u ->
u.getStatus()
}.min() ?: UpstreamAvailability.UNAVAILABLE
@@ -59,6 +59,6 @@ class SubscribeStatus(
.build()
}
class ChainSubscription(val chain: Chain, val up: AggregatedUpstream, val avail: UpstreamAvailability)
class ChainSubscription(val chain: Chain, val up: AggregatedUpstream<*, *>, val avail: UpstreamAvailability)
}

View File

@@ -17,15 +17,20 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.Wei
import io.infinitape.etherjar.rpc.Commands
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.BlockTag
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.scheduling.annotation.Scheduled
@@ -43,12 +48,12 @@ import java.util.concurrent.atomic.AtomicLong
import javax.annotation.PostConstruct
@Service
class TrackAddress(
class TrackEthereumAddress(
@Autowired private val upstreams: Upstreams,
@Autowired private val upstreamScheduler: Scheduler
) {
private val log = LoggerFactory.getLogger(TrackAddress::class.java)
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
private val seq = AtomicLong(0)
@@ -94,6 +99,9 @@ class TrackAddress(
private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> {
val chain = Chain.byId(request.asset.chainValue)
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
}
if (!upstreams.isAvailable(chain)) {
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
}
@@ -122,6 +130,10 @@ class TrackAddress(
fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue)
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
return@flatMapMany Flux.error<BlockchainOuterClass.AddressBalance>(SilentException.UnsupportedBlockchain(request.asset.chainValue))
}
val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
initializeSubscription(request, bus)
.flatMap { tracked ->
@@ -175,7 +187,8 @@ class TrackAddress(
}
fun getBalance(addr: SimpleAddress): Mono<Wei> {
val up = upstreams.getUpstream(addr.chain) ?: return Mono.error(SilentException.UnsupportedBlockchain(addr.chain))
val up = upstreams.getUpstream(addr.chain) as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>?
?: return Mono.error(SilentException.UnsupportedBlockchain(addr.chain))
return up.getApi(Selector.empty)
.flatMap { api -> api.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) }
.timeout(Defaults.timeout)

View File

@@ -18,10 +18,13 @@ package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId
@@ -40,7 +43,6 @@ import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux
import reactor.core.scheduler.Scheduler
import reactor.util.function.Tuples
import java.lang.Exception
import java.math.BigInteger
import java.time.Duration
import java.time.Instant
@@ -53,7 +55,7 @@ import kotlin.math.max
import kotlin.math.min
@Service
class TrackTx(
class TrackEthereumTx(
@Autowired private val upstreams: Upstreams,
@Autowired private val upstreamScheduler: Scheduler
) {
@@ -67,7 +69,7 @@ class TrackTx(
private val PING_PERIOD = Duration.ofMinutes(5)
}
private val log = LoggerFactory.getLogger(TrackTx::class.java)
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedTx>>()
private val seq = AtomicLong(0)
@@ -142,6 +144,9 @@ class TrackTx(
fun prepareTracking(request: BlockchainOuterClass.TxStatusRequest): TxDetails {
val chain = Chain.byId(request.chainValue)
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
throw SilentException.UnsupportedBlockchain(request.chainValue)
}
if (!clients.containsKey(chain)) {
throw SilentException.UnsupportedBlockchain(chain)
}
@@ -208,7 +213,7 @@ class TrackTx(
}
private fun loadWeight(tx: TxDetails): Mono<TxDetails> {
val upstream = upstreams.getUpstream(tx.chain)
val upstream = upstreams.getUpstream(tx.chain) as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>?
?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
return upstream.getApi(Selector.empty)
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(tx.status.blockHash)) }
@@ -219,7 +224,7 @@ class TrackTx(
}
}
fun updateFromBlock(upstream: Upstream, tx: TxDetails, it: TransactionJson): Mono<TxDetails> {
fun updateFromBlock(upstream: Upstream<EthereumApi, BlockJson<TransactionRefJson>>, tx: TxDetails, it: TransactionJson): Mono<TxDetails> {
return if (it.blockNumber != null && it.blockHash != null && it.blockHash != ZERO_BLOCK) {
val updated = tx.withStatus(
blockHash = it.blockHash,
@@ -250,7 +255,8 @@ class TrackTx(
private fun checkForUpdate(tx: TxDetails): Mono<TxDetails> {
val initialStatus = tx.status
val upstream = upstreams.getUpstream(tx.chain) ?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
val upstream = upstreams.getUpstream(tx.chain) as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>?
?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
val execution = upstream.getApi(Selector.empty)
.flatMap { api -> api.executeAndConvert(Commands.eth().getTransaction(tx.txid)) }
return execution

View File

@@ -16,9 +16,9 @@
package io.emeraldpay.dshackle.startup
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
import io.emeraldpay.dshackle.upstream.CurrentUpstreams
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
@@ -27,16 +27,13 @@ import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment
import org.springframework.stereotype.Repository
import java.net.URI
import java.util.*
import javax.annotation.PostConstruct
import kotlin.collections.HashMap
import kotlin.system.exitProcess
@Repository
open class ConfiguredUpstreams(
@@ -59,21 +56,26 @@ open class ConfiguredUpstreams(
@PostConstruct
fun start() {
log.debug("Starting upstreams")
val defaultOptions = buildDefaultOptions(config)
config.upstreams.forEach { up ->
log.debug("Start upstream ${up.id}")
if (up.connection is UpstreamsConfig.GrpcConnection) {
val options = up.options ?: UpstreamsConfig.Options()
buildGrpcUpstream(up as UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options)
buildGrpcUpstream(up.cast(UpstreamsConfig.GrpcConnection::class.java), options)
} else {
val chain = chainNames[up.chain]
if (chain == null) {
log.error("Chain not supported: ${up.chain}")
log.error("Chain is unknown: ${up.chain}")
return@forEach
}
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
log.error("Chain is unsupported: ${up.chain}")
return@forEach
}
val options = (up.options ?: UpstreamsConfig.Options())
.merge(defaultOptions[chain] ?: UpstreamsConfig.Options.getDefaults())
buildEthereumUpstream(up as UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>, chain, options)
buildEthereumUpstream(up.cast(UpstreamsConfig.EthereumConnection::class.java), chain, options)
}
}
}

View File

@@ -20,15 +20,38 @@ import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.grpc.Chain
/**
* An update event to the list of currently available upstreams.
*/
class UpstreamChange(
/**
* Target blockchain
*/
val chain: Chain,
val upstream: Upstream,
/**
* Corresponding upstream
*/
val upstream: Upstream<*, *>,
/**
* Type of the change
*/
val type: ChangeType
): CachesEnabled {
enum class ChangeType {
/**
* Upstream just added
*/
ADDED,
/**
* Upstream become available after being temporally off
*/
REVALIDATED,
STALE,
/**
* Upstream is removed (it still doesn't mean it wouldn't return again after some reconfiguration)
*/
REMOVED,
}

View File

@@ -31,19 +31,22 @@ import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import kotlin.concurrent.withLock
abstract class AggregatedUpstream(
/**
* Aggregation of multiple upstreams responding to a single blockchain
*/
abstract class AggregatedUpstream<U : UpstreamApi, B>(
private val objectMapper: ObjectMapper,
val caches: Caches
): Upstream, Lifecycle {
) : Upstream<U, B>, Lifecycle {
private var cacheSubscription: Disposable? = null
var cache: CachingEthereumApi = CachingEthereumApi.empty()
private val reconfigLock = ReentrantLock()
private var callMethods: CallMethods? = null
abstract fun getAll(): List<Upstream>
abstract fun addUpstream(upstream: Upstream)
abstract fun getApis(matcher: Selector.Matcher): ApiSource
abstract fun getAll(): List<Upstream<U, B>>
abstract fun addUpstream(upstream: Upstream<U, B>)
abstract fun getApis(matcher: Selector.Matcher): ApiSource<U>
fun onUpstreamsUpdated() {
reconfigLock.withLock {
@@ -78,24 +81,6 @@ abstract class AggregatedUpstream(
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
}
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
class FilterBestAvailability(): Predicate<UpstreamStatus> {
private val lastRef = AtomicReference<UpstreamStatus>()
override fun test(t: UpstreamStatus): Boolean {
val last = lastRef.get()
val changed = last == null
|| t.status > last.status
|| (last.upstream == t.upstream && t.status != last.status)
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
if (changed) {
lastRef.set(t)
}
return changed
}
}
override fun start() {
}
@@ -114,4 +99,24 @@ abstract class AggregatedUpstream(
}
}
// --------------------------------------------------------------------------------------------------------
class UpstreamStatus<H>(val upstream: Upstream<UpstreamApi, H>, val status: UpstreamAvailability, val ts: Instant = Instant.now())
class FilterBestAvailability() : Predicate<UpstreamStatus<*>> {
private val lastRef = AtomicReference<UpstreamStatus<*>>()
override fun test(t: UpstreamStatus<*>): Boolean {
val last = lastRef.get()
val changed = last == null
|| t.status > last.status
|| (last.upstream == t.upstream && t.status != last.status)
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
if (changed) {
lastRef.set(t)
}
return changed
}
}
}

View File

@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import org.reactivestreams.Publisher
interface ApiSource: Publisher<DirectEthereumApi> {
interface ApiSource<U : UpstreamApi> : Publisher<U> {
fun resolve()
fun request(tries: Int)

View File

@@ -17,38 +17,34 @@ package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHeadMerge
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
import reactor.core.Disposable
import reactor.core.publisher.Mono
import java.lang.IllegalStateException
import java.time.Duration
open class ChainUpstreams (
/**
* General interface to upstream(s) to a single chain
*/
abstract class ChainUpstreams<U : UpstreamApi, B>(
val chain: Chain,
private val upstreams: MutableList<Upstream>,
private val upstreams: MutableList<Upstream<U, B>>,
caches: Caches,
objectMapper: ObjectMapper
) : AggregatedUpstream(objectMapper, caches), Lifecycle {
) : AggregatedUpstream<U, B>(objectMapper, caches), Lifecycle {
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
private var seq = 0
private var head: EthereumHead? = null
private var lagObserver: HeadLagObserver? = null
protected var lagObserver: HeadLagObserver<U, B>? = null
private var subscription: Disposable? = null
init {
if (upstreams.size > 0) {
head = updateHead()
onUpstreamsUpdated()
}
open fun init() {
onUpstreamsUpdated()
}
abstract fun updateHead(): Head<B>
abstract fun setHead(head: Head<B>)
override fun getId(): String {
return "!all:${chain.chainCode}"
}
@@ -68,7 +64,7 @@ open class ChainUpstreams (
super.stop()
subscription?.dispose()
subscription = null
head?.let {
getHead().let {
if (it is Lifecycle) {
it.stop()
}
@@ -76,50 +72,24 @@ open class ChainUpstreams (
lagObserver?.stop()
}
internal fun updateHead(): EthereumHead {
head?.let {
if (it is Lifecycle) {
it.stop()
}
}
lagObserver?.stop()
lagObserver = null
val head = if (upstreams.size == 1) {
val upstream = upstreams.first()
upstream.setLag(0)
upstream.getHead()
} else {
val newHead = EthereumHeadMerge(upstreams.map { it.getHead() }).apply {
this.start()
}
val lagObserver = HeadLagObserver(newHead, upstreams).apply {
this.start()
}
this.lagObserver = lagObserver
newHead
}
onHeadUpdated(head)
return head
}
override fun getAll(): List<Upstream> {
override fun getAll(): List<Upstream<U, B>> {
return upstreams
}
override fun addUpstream(upstream: Upstream) {
override fun addUpstream(upstream: Upstream<U, B>) {
upstreams.add(upstream)
head = updateHead()
setHead(updateHead())
onUpstreamsUpdated()
}
fun removeUpstream(id: String) {
if (upstreams.removeIf { it.getId() == id }) {
head = updateHead()
setHead(updateHead())
onUpstreamsUpdated()
}
}
override fun getApis(matcher: Selector.Matcher): ApiSource {
override fun getApis(matcher: Selector.Matcher): ApiSource<U> {
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
@@ -127,15 +97,11 @@ open class ChainUpstreams (
return FilteredApis(upstreams, matcher, i)
}
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> {
override fun getApi(matcher: Selector.Matcher): Mono<U> {
val apis = getApis(matcher)
apis.request(1)
return Mono.from(apis)
.switchIfEmpty(Mono.error<DirectEthereumApi>(Exception("No API available")))
}
override fun getHead(): EthereumHead {
return head!!
.switchIfEmpty(Mono.error<U>(Exception("No API available")))
}
override fun setLag(lag: Long) {
@@ -145,28 +111,5 @@ open class ChainUpstreams (
return 0
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreams.flatMap { it.getLabels() }
}
fun printStatus() {
var height: Long? = null
try {
height = head!!.getFlux().next().block(Duration.ofSeconds(1))?.number
} catch (e: IllegalStateException) {
//timout
} catch (e: Exception) {
log.warn("Head processing error: ${e.javaClass} ${e.message}")
}
val statuses = upstreams.map { it.getStatus() }
.groupBy { it }
.map { "${it.key.name}/${it.value.size}" }
.joinToString(",")
val lag = upstreams.map { it.getLag() }
.joinToString(", ")
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
}
abstract fun printStatus()
}

View File

@@ -16,13 +16,17 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.startup.UpstreamChange
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.scheduling.annotation.Scheduled
@@ -42,22 +46,23 @@ class CurrentUpstreams(
private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java)
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams>()
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams<*, *>>()
private val chainsBus = TopicProcessor.create<Chain>()
private val callTargets = HashMap<Chain, QuorumBasedMethods>()
private val callTargets = HashMap<Chain, CallMethods>()
private val updateLock = ReentrantLock()
fun update(change: UpstreamChange) {
updateLock.withLock {
val chain = change.chain
val up = change.upstream
val current = chainMapping[chain]
.cast(EthereumUpstream::class.java, EthereumApi::class.java, BlockJson::class.java) as Upstream<EthereumApi, BlockJson<TransactionRefJson>>
val current = chainMapping[chain] as ChainUpstreams<EthereumApi, BlockJson<TransactionRefJson>>?
if (change.type == UpstreamChange.ChangeType.REMOVED) {
current?.removeUpstream(up.getId())
log.info("Upstream ${change.upstream.getId()} with chain $chain has been removed")
} else {
if (current == null) {
val created = ChainUpstreams(chain, ArrayList<Upstream>(), cachesFactory.getCaches(chain), objectMapper)
val created = EthereumChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper)
if (up is CachesEnabled) {
up.setCaches(created.caches)
}
@@ -79,7 +84,7 @@ class CurrentUpstreams(
}
}
override fun getUpstream(chain: Chain): AggregatedUpstream? {
override fun getUpstream(chain: Chain): AggregatedUpstream<*, *>? {
return chainMapping[chain]
}
@@ -103,8 +108,8 @@ class CurrentUpstreams(
return callTargets[chain] ?: return setupDefaultMethods(chain)
}
fun setupDefaultMethods(chain: Chain): QuorumBasedMethods {
val created = QuorumBasedMethods(objectMapper, chain)
fun setupDefaultMethods(chain: Chain): CallMethods {
val created = DefaultEthereumMethods(objectMapper, chain)
callTargets[chain] = created
return created
}

View File

@@ -19,10 +19,10 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
import java.util.concurrent.atomic.AtomicReference
abstract class DefaultUpstream(
abstract class DefaultUpstream<U : UpstreamApi, B>(
defaultLag: Long,
defaultAvail: UpstreamAvailability
) : Upstream {
) : Upstream<U, B> {
constructor() : this(Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE)

View File

@@ -15,7 +15,6 @@
*/
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import org.reactivestreams.Subscriber
import reactor.core.publisher.EmitterProcessor
import reactor.core.publisher.Flux
@@ -26,28 +25,28 @@ import kotlin.math.pow
import kotlin.math.roundToLong
import kotlin.random.Random
class FilteredApis(
allUpstreams: List<Upstream>,
class FilteredApis<U : UpstreamApi>(
allUpstreams: List<Upstream<U, *>>,
private val matcher: Selector.Matcher,
pos: Int,
private val repeatLimit: Long,
jitter: Int
): ApiSource {
) : ApiSource<U> {
companion object {
private const val DEFAULT_DELAY_STEP = 100
private const val MAX_WAIT_MILLIS = 5000L
}
constructor(allUpstreams: List<Upstream>,
constructor(allUpstreams: List<Upstream<U, *>>,
matcher: Selector.Matcher,
pos: Int): this(allUpstreams, matcher, pos, 10, 7)
pos: Int) : this(allUpstreams, matcher, pos, 10, 7)
constructor(allUpstreams: List<Upstream>,
matcher: Selector.Matcher): this(allUpstreams, matcher, 0, 10, 10)
constructor(allUpstreams: List<Upstream<U, *>>,
matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
private val delay: Int
private val upstreams: List<Upstream>
private val upstreams: List<Upstream<UpstreamApi, *>>
private val control = EmitterProcessor.create<Boolean>(32, false)
@@ -75,18 +74,18 @@ class FilteredApis(
return Duration.ofMillis(time)
}
override fun subscribe(subscriber: Subscriber<in DirectEthereumApi>) {
override fun subscribe(subscriber: Subscriber<in U>) {
val first = Flux.fromIterable(upstreams)
val retries = (1 until repeatLimit).map { r ->
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
}.let { Flux.concat(it) }
Flux.concat(first, retries)
.filter(Upstream::isAvailable)
.filter(Upstream<UpstreamApi, *>::isAvailable)
.filter(matcher::matches)
.flatMap { it.getApi(matcher) }
.zipWith(control).map { it.t1 }
.subscribe(subscriber)
.subscribe(subscriber as Subscriber<in UpstreamApi>)
}
override fun resolve() {

View File

@@ -18,6 +18,6 @@ package io.emeraldpay.dshackle.upstream
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
interface Head<T> {
fun getFlux(): Flux<T>
interface Head<out T> {
fun getFlux(): Flux<out T>
}

View File

@@ -15,23 +15,21 @@
*/
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
import reactor.core.Disposable
import reactor.core.publisher.Flux
import reactor.core.publisher.toFlux
import reactor.util.function.Tuple2
import reactor.util.function.Tuples
import java.time.Duration
class HeadLagObserver (
private val master: EthereumHead,
private val followers: Collection<Upstream>
): Lifecycle {
/**
* Observer group of upstreams and defined a distance in blocks (lag) between a leader (best height/difficulty) and
* other upstreams.
*/
abstract class HeadLagObserver<A : UpstreamApi, B>(
private val master: Head<B>,
private val followers: Collection<Upstream<A, B>>
) : Lifecycle {
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
@@ -50,7 +48,7 @@ class HeadLagObserver (
current = null
}
private fun subscription(): Flux<Unit> {
fun subscription(): Flux<Unit> {
return master.getFlux()
.flatMap(this::probeFollowers)
.map { item ->
@@ -58,38 +56,29 @@ class HeadLagObserver (
}
}
fun probeFollowers(top: BlockJson<TransactionRefJson>): Flux<Tuple2<Long, Upstream>> {
return followers.toFlux()
fun probeFollowers(top: B): Flux<Tuple2<Long, Upstream<A, B>>> {
return Flux.fromIterable(followers)
.parallel(followers.size)
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
.sequential()
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
}
fun getCurrentBlocks(up: Upstream): Flux<BlockJson<TransactionRefJson>> {
val head = up.getHead()
return head.getFlux().take(Duration.ofSeconds(1))
}
abstract fun getCurrentBlocks(up: Upstream<A, B>): Flux<B>
fun mapLagging(top: BlockJson<TransactionRefJson>, up: Upstream, blocks: Flux<BlockJson<TransactionRefJson>>): Flux<Tuple2<Long, Upstream>> {
fun mapLagging(top: B, up: Upstream<A, B>, blocks: Flux<B>): Flux<Tuple2<Long, Upstream<A, B>>> {
return blocks
.map { extractDistance(top, it) }
.takeUntil{ lag -> lag <= 0L }
.takeUntil { lag -> lag <= 0L }
.map { Tuples.of(it, up) }
.doOnError { t ->
log.warn("Failed to find distance for $up", t)
}
}
fun extractDistance(top: BlockJson<TransactionRefJson>, curr: BlockJson<TransactionRefJson>): Long {
return when {
curr.number > top.number -> if (curr.totalDifficulty >= top.totalDifficulty) 0 else forkDistance(top, curr)
curr.number == top.number -> if (curr.totalDifficulty == top.totalDifficulty) 0 else forkDistance(top, curr)
else -> top.number - curr.number
}
}
abstract fun extractDistance(top: B, curr: B): Long
fun forkDistance(top: BlockJson<TransactionRefJson>, curr: BlockJson<TransactionRefJson>): Long {
fun forkDistance(top: B, curr: B): Long {
//TODO look for common ancestor? though it may be a corruption
return 6
}

View File

@@ -95,13 +95,13 @@ class Selector {
}
interface Matcher {
fun matches(up: Upstream): Boolean
fun matches(up: Upstream<UpstreamApi, *>): Boolean
}
class MultiMatcher(
private val matchers: Collection<Matcher>
): Matcher {
override fun matches(up: Upstream): Boolean {
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
return matchers.all { it.matches(up) }
}
@@ -113,21 +113,22 @@ class Selector {
class MethodMatcher(
val method: String
): Matcher {
override fun matches(up: Upstream): Boolean {
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
return up.getMethods().isAllowed(method)
}
}
abstract class LabelSelectorMatcher: Matcher {
override fun matches(up: Upstream): Boolean {
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
return up.getLabels().any(this::matches)
}
abstract fun matches(labels: UpstreamsConfig.Labels): Boolean
abstract fun asProto(): BlockchainOuterClass.Selector?
}
class EmptyMatcher: Matcher {
override fun matches(up: Upstream): Boolean {
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
return true
}
}
@@ -142,7 +143,7 @@ class Selector {
return null
}
override fun matches(up: Upstream): Boolean {
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
return true
}
}

View File

@@ -22,16 +22,18 @@ import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
interface Upstream {
interface Upstream<out T : UpstreamApi, out B> {
fun isAvailable(): Boolean
fun getStatus(): UpstreamAvailability
fun observeStatus(): Flux<UpstreamAvailability>
fun getHead(): EthereumHead
fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi>
fun getHead(): Head<B>
fun getApi(matcher: Selector.Matcher): Mono<out T>
fun getOptions(): UpstreamsConfig.Options
fun setLag(lag: Long)
fun getLag(): Long
fun getLabels(): Collection<UpstreamsConfig.Labels>
fun getMethods(): CallMethods
fun getId(): String
fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
import reactor.core.publisher.Mono
/**
* A general interface to make a request to an Upstream API
*/
interface UpstreamApi {
/**
* @param id an internal uniq id, if multiple requests are made in batch
* @param method JSON RPC method name
* @param params JSON RPC parameters, must be serializable into a JSON array
*/
fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
}

View File

@@ -20,7 +20,7 @@ import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux
interface Upstreams {
fun getUpstream(chain: Chain): AggregatedUpstream?
fun getUpstream(chain: Chain): AggregatedUpstream<*, *>?
fun getAvailable(): List<Chain>
fun observeChains(): Flux<Chain>
fun getDefaultMethods(chain: Chain): CallMethods

View File

@@ -26,7 +26,7 @@ import java.util.*
* Default configuration for Ethereum based RPC. Defines optimal Quorum strategies for different methods, and provides
* hardcoded results for base methods, such as `net_version`, `web3_clientVersion` and similar
*/
class QuorumBasedMethods(
class DefaultEthereumMethods(
private val objectMapper: ObjectMapper,
private val chain: Chain
) : CallMethods {
@@ -102,6 +102,7 @@ class QuorumBasedMethods(
override fun isAllowed(method: String): Boolean {
return allowedMethods.contains(method)
}
override fun isHardcoded(method: String): Boolean {
return hardcodedMethods.contains(method)
}

View File

@@ -17,23 +17,24 @@ package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamApi
import io.infinitape.etherjar.rpc.*
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import java.io.InputStream
abstract class EthereumApi(
objectMapper: ObjectMapper
) {
) : UpstreamApi {
companion object {
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
}
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
var upstream: Upstream? = null
abstract fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
var upstream: Upstream<EthereumApi, BlockJson<TransactionRefJson>>? = null
fun <JS, RS> execute(rpcCall: RpcCall<JS, RS>): Mono<ByteArray> {
return execute(0, rpcCall.method, rpcCall.params as List<Any>)

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
import java.lang.IllegalStateException
import java.time.Duration
class EthereumChainUpstreams(
chain: Chain,
val upstreams: MutableList<EthereumUpstream>,
caches: Caches,
objectMapper: ObjectMapper
) : ChainUpstreams<EthereumApi, BlockJson<TransactionRefJson>>(chain, upstreams as MutableList<Upstream<EthereumApi, BlockJson<TransactionRefJson>>>, caches, objectMapper) {
companion object {
private val log = LoggerFactory.getLogger(EthereumChainUpstreams::class.java)
}
private var head: EthereumHead? = null
init {
this.init()
}
override fun init() {
if (upstreams.size > 0) {
head = updateHead()
}
super.init()
}
override fun getHead(): EthereumHead {
return head!!
}
override fun setHead(head: Head<BlockJson<TransactionRefJson>>) {
this.head = head as EthereumHead
}
override fun updateHead(): EthereumHead {
head?.let {
if (it is Lifecycle) {
it.stop()
}
}
lagObserver?.stop()
lagObserver = null
val head = if (upstreams.size == 1) {
val upstream = upstreams.first()
upstream.setLag(0)
upstream.getHead()
} else {
val newHead = EthereumHeadMerge(upstreams.map { it.getHead() }).apply {
this.start()
}
val lagObserver = EthereumHeadLagObserver(newHead, upstreams).apply {
this.start()
}
this.lagObserver = lagObserver
newHead
}
onHeadUpdated(head)
return head
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreams.flatMap { it.getLabels() }
}
override fun printStatus() {
var height: Long? = null
try {
height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.number
} catch (e: IllegalStateException) {
//timout
} catch (e: Exception) {
log.warn("Head processing error: ${e.javaClass} ${e.message}")
}
val statuses = upstreams.map { it.getStatus() }
.groupBy { it }
.map { "${it.key.name}/${it.value.size}" }
.joinToString(",")
val lag = upstreams.map { it.getLag() }
.joinToString(", ")
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
}
@SuppressWarnings("unchecked")
override fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType")
}
if (!blockType.isAssignableFrom(BlockJson::class.java)) {
throw ClassCastException("Cannot cast ${BlockJson::class.java} to $blockType")
}
return this as T
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.HeadLagObserver
import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import java.time.Duration
class EthereumHeadLagObserver(
master: EthereumHead,
followers: Collection<Upstream<EthereumApi, BlockJson<TransactionRefJson>>>
) : HeadLagObserver<EthereumApi, BlockJson<TransactionRefJson>>(master, followers) {
companion object {
private val log = LoggerFactory.getLogger(EthereumHeadLagObserver::class.java)
}
override fun getCurrentBlocks(up: Upstream<EthereumApi, BlockJson<TransactionRefJson>>): Flux<BlockJson<TransactionRefJson>> {
val head = up.getHead()
return Flux.from(head.getFlux()).take(Duration.ofSeconds(1))
}
override fun extractDistance(top: BlockJson<TransactionRefJson>, curr: BlockJson<TransactionRefJson>): Long {
return when {
curr.number > top.number -> if (curr.totalDifficulty >= top.totalDifficulty) 0 else forkDistance(top, curr)
curr.number == top.number -> if (curr.totalDifficulty == top.totalDifficulty) 0 else forkDistance(top, curr)
else -> top.number - curr.number
}
}
}

View File

@@ -23,6 +23,8 @@ import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
import reactor.core.Disposable
@@ -37,9 +39,9 @@ open class EthereumUpstream(
private val options: UpstreamsConfig.Options,
val node: QuorumForLabels.QuorumItem,
private val targets: CallMethods
): DefaultUpstream(), CachesEnabled, Lifecycle {
) : DefaultUpstream<EthereumApi, BlockJson<TransactionRefJson>>(), Upstream<EthereumApi, BlockJson<TransactionRefJson>>, CachesEnabled, Lifecycle {
constructor(id: String, chain: Chain, api: DirectEthereumApi): this(id, chain, api, null,
constructor(id: String, chain: Chain, api: DirectEthereumApi) : this(id, chain, api, null,
UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
DirectCallMethods())
@@ -133,4 +135,18 @@ open class EthereumUpstream(
return targets
}
@Suppress("unchecked")
override fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType")
}
if (!blockType.isAssignableFrom(BlockJson::class.java)) {
throw ClassCastException("Cannot cast ${BlockJson::class.java} to $blockType")
}
return this as T
}
}

View File

@@ -30,6 +30,7 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash
@@ -51,16 +52,16 @@ import java.util.concurrent.atomic.AtomicReference
import java.util.function.Function
import kotlin.collections.ArrayList
open class GrpcUpstream(
open class EthereumGrpcUpstream(
private val parentId: String,
private val chain: Chain,
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val objectMapper: ObjectMapper,
private val rpcClient: ReactorEmeraldClient
): DefaultUpstream(), CachesEnabled, Lifecycle {
) : DefaultUpstream<EthereumApi, BlockJson<TransactionRefJson>>(), CachesEnabled, Lifecycle {
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
private val log = LoggerFactory.getLogger(GrpcUpstream::class.java)
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
private var caches: Caches? = null
private val options = UpstreamsConfig.Options.getDefaults()
@@ -214,4 +215,18 @@ open class GrpcUpstream(
this.caches = caches
}
@SuppressWarnings("unchecked")
override fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType")
}
if (!blockType.isAssignableFrom(BlockJson::class.java)) {
throw ClassCastException("Cannot cast ${BlockJson::class.java} to $blockType")
}
return this as T
}
}

View File

@@ -18,10 +18,10 @@ package io.emeraldpay.dshackle.upstream.grpc
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.startup.UpstreamChange
import io.emeraldpay.grpc.Chain
@@ -54,7 +54,7 @@ class GrpcUpstreams(
var timeout = Defaults.timeout
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
private val known = HashMap<Chain, GrpcUpstream>()
private val known = HashMap<Chain, EthereumGrpcUpstream>()
private val lock = ReentrantLock()
private var grpcTransport: ReactorEmeraldClient? = null
@@ -117,7 +117,7 @@ class GrpcUpstreams(
}.map { chainDetails ->
val chain = Chain.byId(chainDetails.chain.number)
val up = getOrCreate(chain)
(up.upstream as GrpcUpstream).init(chainDetails)
(up.upstream as EthereumGrpcUpstream).init(chainDetails)
up
}
@@ -155,10 +155,13 @@ class GrpcUpstreams(
}
fun getOrCreate(chain: Chain): UpstreamChange {
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
throw IllegalArgumentException("Unsupported blockchain: $chain")
}
lock.withLock {
val current = known[chain]
return if (current == null) {
val created = GrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
created.timeout = this.timeout
known[chain] = created
created.start()
@@ -169,7 +172,7 @@ class GrpcUpstreams(
}
}
fun get(chain: Chain): GrpcUpstream {
fun get(chain: Chain): EthereumGrpcUpstream {
return known[chain]!!
}
}

View File

@@ -0,0 +1,22 @@
package io.emeraldpay.dshackle
import io.emeraldpay.grpc.Chain
import spock.lang.Specification
class BlockchainTypeSpec extends Specification {
def "Correct type for ethereum"() {
expect:
BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM
where:
chain << [Chain.ETHEREUM, Chain.ETHEREUM_CLASSIC, Chain.TESTNET_KOVAN, Chain.TESTNET_MORDEN]
}
def "Correct type for bitcoin"() {
expect:
BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN
where:
chain << [Chain.BITCOIN, Chain.TESTNET_BITCOIN]
}
}

View File

@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.EthereumUpstreamMock
import io.emeraldpay.dshackle.test.UpstreamsMock
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId
@@ -38,7 +39,7 @@ class StreamHeadSpec extends Specification {
def "Errors on unavailable chain"() {
setup:
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(Upstream))
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(EthereumUpstream))
def streamHead = new StreamHead(upstreams)
when:
def flux = streamHead.add(

View File

@@ -33,7 +33,7 @@ import spock.lang.Specification
import java.time.Duration
class TrackAddressSpec extends Specification {
class TrackEthereumAddressSpec extends Specification {
def chain = Common.ChainRef.CHAIN_ETHEREUM
def address1 = "0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f"
@@ -59,7 +59,7 @@ class TrackAddressSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackAddress trackAddress = new TrackAddress(upstreams, Schedulers.immediate())
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams, Schedulers.immediate())
trackAddress.init()
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
@@ -101,7 +101,7 @@ class TrackAddressSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackAddress trackAddress = new TrackAddress(upstreams, Schedulers.immediate())
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams, Schedulers.immediate())
trackAddress.init()
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")

View File

@@ -36,7 +36,7 @@ import spock.lang.Specification
import java.time.Duration
import java.time.Instant
class TrackTxSpec extends Specification {
class TrackEthereumTxSpec extends Specification {
def chain = Common.ChainRef.CHAIN_ETHEREUM
def txId = "0xba61ce4672751fd6086a9ac2b55547a5555af17535b6c0334ede2ecb6d64070a"
@@ -45,8 +45,8 @@ class TrackTxSpec extends Specification {
def "Gives details for an old transaction"() {
setup:
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
.setChain(chain)
.setConfirmationLimit(6)
.setChain(chain)
.setConfirmationLimit(6)
.setTxId(txId)
.build()
@@ -91,7 +91,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
@@ -123,7 +123,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
apiMock.answer("eth_getTransactionByHash", [txId], null)
@@ -180,7 +180,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
@@ -279,7 +279,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
@@ -320,7 +320,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
@@ -341,7 +341,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init()
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()

View File

@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
@@ -33,11 +33,11 @@ class EthereumUpstreamMock extends EthereumUpstream {
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) {
this(chain, api, new QuorumBasedMethods(TestingCommons.objectMapper(), chain))
this(chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
}
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) {
this(id, chain, api, new QuorumBasedMethods(TestingCommons.objectMapper(), chain))
this(id, chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
}
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) {

View File

@@ -27,6 +27,7 @@ import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.ChainUpstreams
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.JacksonRpcConverter
@@ -75,7 +76,7 @@ class TestingCommons {
}
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) {
return new ChainUpstreams(Chain.ETHEREUM, [up], Caches.default(), objectMapper())
return new EthereumChainUpstreams(Chain.ETHEREUM, [up], Caches.default(), objectMapper())
}
static CachesFactory emptyCaches() {

View File

@@ -18,16 +18,17 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.ChainUpstreams
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.grpc.Chain
import org.jetbrains.annotations.NotNull
import reactor.core.publisher.Flux
class UpstreamsMock implements Upstreams {
private Map<Chain, QuorumBasedMethods> target = [:]
private Map<Chain, DefaultEthereumMethods> target = [:]
private Map<Chain, AggregatedUpstream> upstreams = [:]
UpstreamsMock(Chain chain, Upstream up) {
@@ -40,7 +41,7 @@ class UpstreamsMock implements Upstreams {
AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
if (!upstreams.containsKey(chain)) {
upstreams[chain] = new ChainUpstreams(chain, [up], Caches.default(), TestingCommons.objectMapper())
upstreams[chain] = new EthereumChainUpstreams(chain, [up], Caches.default(), TestingCommons.objectMapper())
} else {
upstreams[chain].addUpstream(up)
}
@@ -63,9 +64,9 @@ class UpstreamsMock implements Upstreams {
}
@Override
QuorumBasedMethods getDefaultMethods(@NotNull Chain chain) {
DefaultEthereumMethods getDefaultMethods(@NotNull Chain chain) {
if (target[chain] == null) {
QuorumBasedMethods targets = new QuorumBasedMethods(TestingCommons.objectMapper(), chain)
DefaultEthereumMethods targets = new DefaultEthereumMethods(TestingCommons.objectMapper(), chain)
target[chain] = targets
}
return target[chain]

View File

@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.test.EthereumUpstreamMock
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.grpc.Chain
import spock.lang.Specification
@@ -30,7 +31,7 @@ class AggregatedUpstreamSpec extends Specification {
setup:
def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test1", "eth_test2"]))
def up2 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test2", "eth_test3"]))
def aggr = new ChainUpstreams(Chain.ETHEREUM, [up1, up2], Caches.default(), TestingCommons.objectMapper())
def aggr = new EthereumChainUpstreams(Chain.ETHEREUM, [up1, up2], Caches.default(), TestingCommons.objectMapper())
when:
aggr.onUpstreamsUpdated()
def act = aggr.getMethods()

View File

@@ -19,7 +19,7 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.test.EthereumApiStub
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
@@ -35,7 +35,7 @@ class FilteredApisSpec extends Specification {
def rpcClient = Stub(ReactorRpcClient)
def objectMapper = TestingCommons.objectMapper()
def ethereumTargets = new QuorumBasedMethods(objectMapper, Chain.ETHEREUM)
def ethereumTargets = new DefaultEthereumMethods(objectMapper, Chain.ETHEREUM)
def "Verifies labels"() {
setup:

View File

@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import io.emeraldpay.dshackle.upstream.HeadLagObserver
import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.rpc.json.BlockJson
import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
@@ -25,7 +26,7 @@ import spock.lang.Specification
import java.time.Duration
class HeadLagObserverSpec extends Specification {
class EthereumHeadLagObserverSpec extends Specification {
def "Updates lag distance"() {
setup:
@@ -64,7 +65,7 @@ class HeadLagObserverSpec extends Specification {
1 * up2.setLag(1)
1 * up2.setLag(0)
HeadLagObserver observer = new HeadLagObserver(master, [up1, up2])
HeadLagObserver observer = new EthereumHeadLagObserver(master, [up1, up2])
when:
def act = observer.subscription().take(Duration.ofMillis(1200))
@@ -78,7 +79,7 @@ class HeadLagObserverSpec extends Specification {
def "Probes until there is no difference"() {
setup:
EthereumHead master = Mock()
HeadLagObserver observer = new HeadLagObserver(master, [])
HeadLagObserver observer = new EthereumHeadLagObserver(master, [])
Upstream up = Mock()
def blocks = [100, 101, 102].collect { i ->
@@ -103,7 +104,7 @@ class HeadLagObserverSpec extends Specification {
def "Correct distance"() {
setup:
EthereumHead master = Mock()
HeadLagObserver observer = new HeadLagObserver(master, [])
HeadLagObserver observer = new EthereumHeadLagObserver(master, [])
expect:
def top = new BlockJson().with {
it.number = topHeight

View File

@@ -34,7 +34,7 @@ import spock.lang.Specification
import java.time.Duration
import java.util.concurrent.CompletableFuture
class GrpcUpstreamSpec extends Specification {
class EthereumGrpcUpstreamSpec extends Specification {
MockServer mockServer = new MockServer()
ObjectMapper objectMapper = TestingCommons.objectMapper()
@@ -70,7 +70,7 @@ class GrpcUpstreamSpec extends Specification {
}
})
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
def upstream = new GrpcUpstream("test", chain, client, objectMapper, transport)
def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, transport)
upstream.setLag(0)
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
.addAllSupportedMethods(["eth_getBlockByHash"])
@@ -126,7 +126,7 @@ class GrpcUpstreamSpec extends Specification {
}
})
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
def upstream = new GrpcUpstream("test", Chain.ETHEREUM, client, objectMapper, transport)
def upstream = new EthereumGrpcUpstream("test", Chain.ETHEREUM, client, objectMapper, transport)
upstream.setLag(0)
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
.addAllSupportedMethods(["eth_getBlockByHash"])
@@ -186,7 +186,7 @@ class GrpcUpstreamSpec extends Specification {
}
})
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
def upstream = new GrpcUpstream("test", chain, client, objectMapper, transport)
def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, transport)
upstream.setLag(0)
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
.addAllSupportedMethods(["eth_getBlockByHash"])