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 package io.emeraldpay.dshackle.config
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import java.lang.ClassCastException
import java.net.URI import java.net.URI
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -72,6 +73,14 @@ class UpstreamsConfig {
var connection: T? = null var connection: T? = null
val labels = Labels() val labels = Labels()
var methods: Methods? = null 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 open class UpstreamConnection

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ open class NonEmptyQuorum(
return result != null || tries >= maxTries return result != null || tries >= maxTries
} }
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) { override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*, *>) {
tries++ tries++
if (responseValue != null) { if (responseValue != null) {
result = response result = response
@@ -49,10 +49,10 @@ open class NonEmptyQuorum(
return result 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 -> val value = responseValue?.let { str ->
HexQuantity.from(str).value.toLong() HexQuantity.from(str).value.toLong()
} }
@@ -65,11 +65,11 @@ open class NonceQuorum(
return result return result
} }
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) { override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>) {
errors++ errors++
} }
override fun record(error: RpcException, upstream: Upstream) { override fun record(error: RpcException, upstream: Upstream<*, *>) {
errors++ errors++
} }

View File

@@ -34,7 +34,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return result.get() != null 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 val lagging = upstream.getLag() > maxLag
if (!lagging) { if (!lagging) {
result.set(response) result.set(response)
@@ -43,7 +43,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return false 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) return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
} }
override fun record(response: ByteArray, upstream: Upstream): Boolean { override fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean {
try { try {
val value = extractValue(response, clazz) val value = extractValue(response, clazz)
recordValue(response, value, upstream) recordValue(response, value, upstream)
@@ -43,12 +43,12 @@ abstract class ValueAwareQuorum<T>(
return isResolved(); return isResolved();
} }
override fun record(error: RpcException, upstream: Upstream) { override fun record(error: RpcException, upstream: Upstream<*, *>) {
recordError(null, error.rpcMessage, 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.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc 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.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -29,8 +30,8 @@ import reactor.core.publisher.Mono
class BlockchainRpc( class BlockchainRpc(
@Autowired private val nativeCall: NativeCall, @Autowired private val nativeCall: NativeCall,
@Autowired private val streamHead: StreamHead, @Autowired private val streamHead: StreamHead,
@Autowired private val trackTx: TrackTx, @Autowired private val trackEthereumTx: TrackEthereumTx,
@Autowired private val trackAddress: TrackAddress, @Autowired private val trackEthereumAddress: TrackEthereumAddress,
@Autowired private val describe: Describe, @Autowired private val describe: Describe,
@Autowired private val subscribeStatus: SubscribeStatus @Autowired private val subscribeStatus: SubscribeStatus
): ReactorBlockchainGrpc.BlockchainImplBase() { ): ReactorBlockchainGrpc.BlockchainImplBase() {
@@ -46,15 +47,15 @@ class BlockchainRpc(
} }
override fun subscribeTxStatus(request: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> { 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> { 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> { 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> { 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.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream 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.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -47,7 +47,7 @@ class Describe(
val nodes = QuorumForLabels() val nodes = QuorumForLabels()
if (up is EthereumUpstream) { if (up is EthereumUpstream) {
nodes.add(up.node) nodes.add(up.node)
} else if (up is GrpcUpstream) { } else if (up is EthereumGrpcUpstream) {
nodes.add(up.getNodes()) nodes.add(up.getNodes())
} }
nodes.getAll().forEach { node -> nodes.getAll().forEach { node ->

View File

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

View File

@@ -18,9 +18,9 @@ package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -44,11 +44,25 @@ class StreamHead(
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain")) ?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
up.getHead() up.getHead()
.getFlux() .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() return BlockchainOuterClass.ChainHead.newBuilder()
.setChainValue(chain.id) .setChainValue(chain.id)
.setHeight(block.number) .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 -> val available = ups.map { u ->
u.getStatus() u.getStatus()
}.min() ?: UpstreamAvailability.UNAVAILABLE }.min() ?: UpstreamAvailability.UNAVAILABLE
@@ -59,6 +59,6 @@ class SubscribeStatus(
.build() .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.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.Wei import io.infinitape.etherjar.domain.Wei
import io.infinitape.etherjar.rpc.Commands 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.BlockTag
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
@@ -43,12 +48,12 @@ import java.util.concurrent.atomic.AtomicLong
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
@Service @Service
class TrackAddress( class TrackEthereumAddress(
@Autowired private val upstreams: Upstreams, @Autowired private val upstreams: Upstreams,
@Autowired private val upstreamScheduler: Scheduler @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 clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
private val seq = AtomicLong(0) private val seq = AtomicLong(0)
@@ -94,6 +99,9 @@ class TrackAddress(
private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> { private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> {
val chain = Chain.byId(request.asset.chainValue) 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)) { if (!upstreams.isAvailable(chain)) {
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
} }
@@ -122,6 +130,10 @@ class TrackAddress(
fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> { fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request -> 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>() val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
initializeSubscription(request, bus) initializeSubscription(request, bus)
.flatMap { tracked -> .flatMap { tracked ->
@@ -175,7 +187,8 @@ class TrackAddress(
} }
fun getBalance(addr: SimpleAddress): Mono<Wei> { 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) return up.getApi(Selector.empty)
.flatMap { api -> api.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) } .flatMap { api -> api.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) }
.timeout(Defaults.timeout) .timeout(Defaults.timeout)

View File

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

View File

@@ -16,9 +16,9 @@
package io.emeraldpay.dshackle.startup package io.emeraldpay.dshackle.startup
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
import io.emeraldpay.dshackle.upstream.CurrentUpstreams import io.emeraldpay.dshackle.upstream.CurrentUpstreams
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi 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.dshackle.upstream.grpc.GrpcUpstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.net.URI import java.net.URI
import java.util.* import java.util.*
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.system.exitProcess
@Repository @Repository
open class ConfiguredUpstreams( open class ConfiguredUpstreams(
@@ -59,21 +56,26 @@ open class ConfiguredUpstreams(
@PostConstruct @PostConstruct
fun start() { fun start() {
log.debug("Starting upstreams")
val defaultOptions = buildDefaultOptions(config) val defaultOptions = buildDefaultOptions(config)
config.upstreams.forEach { up -> config.upstreams.forEach { up ->
log.debug("Start upstream ${up.id}")
if (up.connection is UpstreamsConfig.GrpcConnection) { if (up.connection is UpstreamsConfig.GrpcConnection) {
val options = up.options ?: UpstreamsConfig.Options() val options = up.options ?: UpstreamsConfig.Options()
buildGrpcUpstream(up as UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options) buildGrpcUpstream(up.cast(UpstreamsConfig.GrpcConnection::class.java), options)
} else { } else {
val chain = chainNames[up.chain] val chain = chainNames[up.chain]
if (chain == null) { 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 return@forEach
} }
val options = (up.options ?: UpstreamsConfig.Options()) val options = (up.options ?: UpstreamsConfig.Options())
.merge(defaultOptions[chain] ?: UpstreamsConfig.Options.getDefaults()) .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.dshackle.upstream.Upstream
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
/**
* An update event to the list of currently available upstreams.
*/
class UpstreamChange( class UpstreamChange(
/**
* Target blockchain
*/
val chain: Chain, val chain: Chain,
val upstream: Upstream, /**
* Corresponding upstream
*/
val upstream: Upstream<*, *>,
/**
* Type of the change
*/
val type: ChangeType val type: ChangeType
): CachesEnabled { ): CachesEnabled {
enum class ChangeType { enum class ChangeType {
/**
* Upstream just added
*/
ADDED, ADDED,
/**
* Upstream become available after being temporally off
*/
REVALIDATED, REVALIDATED,
STALE,
/**
* Upstream is removed (it still doesn't mean it wouldn't return again after some reconfiguration)
*/
REMOVED, REMOVED,
} }

View File

@@ -31,19 +31,22 @@ import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import kotlin.concurrent.withLock 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, private val objectMapper: ObjectMapper,
val caches: Caches val caches: Caches
): Upstream, Lifecycle { ) : Upstream<U, B>, Lifecycle {
private var cacheSubscription: Disposable? = null private var cacheSubscription: Disposable? = null
var cache: CachingEthereumApi = CachingEthereumApi.empty() var cache: CachingEthereumApi = CachingEthereumApi.empty()
private val reconfigLock = ReentrantLock() private val reconfigLock = ReentrantLock()
private var callMethods: CallMethods? = null private var callMethods: CallMethods? = null
abstract fun getAll(): List<Upstream> abstract fun getAll(): List<Upstream<U, B>>
abstract fun addUpstream(upstream: Upstream) abstract fun addUpstream(upstream: Upstream<U, B>)
abstract fun getApis(matcher: Selector.Matcher): ApiSource abstract fun getApis(matcher: Selector.Matcher): ApiSource<U>
fun onUpstreamsUpdated() { fun onUpstreamsUpdated() {
reconfigLock.withLock { reconfigLock.withLock {
@@ -78,24 +81,6 @@ abstract class AggregatedUpstream(
return callMethods ?: throw IllegalStateException("Methods are not initialized yet") 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() { 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 io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
interface ApiSource: Publisher<DirectEthereumApi> { interface ApiSource<U : UpstreamApi> : Publisher<U> {
fun resolve() fun resolve()
fun request(tries: Int) fun request(tries: Int)

View File

@@ -17,38 +17,34 @@ package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches 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 io.emeraldpay.grpc.Chain
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.Disposable
import reactor.core.publisher.Mono 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, val chain: Chain,
private val upstreams: MutableList<Upstream>, private val upstreams: MutableList<Upstream<U, B>>,
caches: Caches, caches: Caches,
objectMapper: ObjectMapper objectMapper: ObjectMapper
) : AggregatedUpstream(objectMapper, caches), Lifecycle { ) : AggregatedUpstream<U, B>(objectMapper, caches), Lifecycle {
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java) private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
private var seq = 0 private var seq = 0
private var head: EthereumHead? = null protected var lagObserver: HeadLagObserver<U, B>? = null
private var lagObserver: HeadLagObserver? = null
private var subscription: Disposable? = null private var subscription: Disposable? = null
init { open fun init() {
if (upstreams.size > 0) { onUpstreamsUpdated()
head = updateHead()
onUpstreamsUpdated()
}
} }
abstract fun updateHead(): Head<B>
abstract fun setHead(head: Head<B>)
override fun getId(): String { override fun getId(): String {
return "!all:${chain.chainCode}" return "!all:${chain.chainCode}"
} }
@@ -68,7 +64,7 @@ open class ChainUpstreams (
super.stop() super.stop()
subscription?.dispose() subscription?.dispose()
subscription = null subscription = null
head?.let { getHead().let {
if (it is Lifecycle) { if (it is Lifecycle) {
it.stop() it.stop()
} }
@@ -76,50 +72,24 @@ open class ChainUpstreams (
lagObserver?.stop() lagObserver?.stop()
} }
internal fun updateHead(): EthereumHead { override fun getAll(): List<Upstream<U, B>> {
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> {
return upstreams return upstreams
} }
override fun addUpstream(upstream: Upstream) { override fun addUpstream(upstream: Upstream<U, B>) {
upstreams.add(upstream) upstreams.add(upstream)
head = updateHead() setHead(updateHead())
onUpstreamsUpdated() onUpstreamsUpdated()
} }
fun removeUpstream(id: String) { fun removeUpstream(id: String) {
if (upstreams.removeIf { it.getId() == id }) { if (upstreams.removeIf { it.getId() == id }) {
head = updateHead() setHead(updateHead())
onUpstreamsUpdated() onUpstreamsUpdated()
} }
} }
override fun getApis(matcher: Selector.Matcher): ApiSource { override fun getApis(matcher: Selector.Matcher): ApiSource<U> {
val i = seq++ val i = seq++
if (seq >= Int.MAX_VALUE / 2) { if (seq >= Int.MAX_VALUE / 2) {
seq = 0 seq = 0
@@ -127,15 +97,11 @@ open class ChainUpstreams (
return FilteredApis(upstreams, matcher, i) 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) val apis = getApis(matcher)
apis.request(1) apis.request(1)
return Mono.from(apis) return Mono.from(apis)
.switchIfEmpty(Mono.error<DirectEthereumApi>(Exception("No API available"))) .switchIfEmpty(Mono.error<U>(Exception("No API available")))
}
override fun getHead(): EthereumHead {
return head!!
} }
override fun setLag(lag: Long) { override fun setLag(lag: Long) {
@@ -145,28 +111,5 @@ open class ChainUpstreams (
return 0 return 0
} }
override fun getLabels(): Collection<UpstreamsConfig.Labels> { abstract fun printStatus()
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]")
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,16 +22,18 @@ import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
interface Upstream { interface Upstream<out T : UpstreamApi, out B> {
fun isAvailable(): Boolean fun isAvailable(): Boolean
fun getStatus(): UpstreamAvailability fun getStatus(): UpstreamAvailability
fun observeStatus(): Flux<UpstreamAvailability> fun observeStatus(): Flux<UpstreamAvailability>
fun getHead(): EthereumHead fun getHead(): Head<B>
fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> fun getApi(matcher: Selector.Matcher): Mono<out T>
fun getOptions(): UpstreamsConfig.Options fun getOptions(): UpstreamsConfig.Options
fun setLag(lag: Long) fun setLag(lag: Long)
fun getLag(): Long fun getLag(): Long
fun getLabels(): Collection<UpstreamsConfig.Labels> fun getLabels(): Collection<UpstreamsConfig.Labels>
fun getMethods(): CallMethods fun getMethods(): CallMethods
fun getId(): String 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 import reactor.core.publisher.Flux
interface Upstreams { interface Upstreams {
fun getUpstream(chain: Chain): AggregatedUpstream? fun getUpstream(chain: Chain): AggregatedUpstream<*, *>?
fun getAvailable(): List<Chain> fun getAvailable(): List<Chain>
fun observeChains(): Flux<Chain> fun observeChains(): Flux<Chain>
fun getDefaultMethods(chain: Chain): CallMethods 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 * 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 * hardcoded results for base methods, such as `net_version`, `web3_clientVersion` and similar
*/ */
class QuorumBasedMethods( class DefaultEthereumMethods(
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val chain: Chain private val chain: Chain
) : CallMethods { ) : CallMethods {
@@ -102,6 +102,7 @@ class QuorumBasedMethods(
override fun isAllowed(method: String): Boolean { override fun isAllowed(method: String): Boolean {
return allowedMethods.contains(method) return allowedMethods.contains(method)
} }
override fun isHardcoded(method: String): Boolean { override fun isHardcoded(method: String): Boolean {
return hardcodedMethods.contains(method) return hardcodedMethods.contains(method)
} }

View File

@@ -17,23 +17,24 @@ package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamApi
import io.infinitape.etherjar.rpc.* import io.infinitape.etherjar.rpc.*
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.io.InputStream import java.io.InputStream
abstract class EthereumApi( abstract class EthereumApi(
objectMapper: ObjectMapper objectMapper: ObjectMapper
) { ) : UpstreamApi {
companion object { companion object {
private val log = LoggerFactory.getLogger(EthereumApi::class.java) private val log = LoggerFactory.getLogger(EthereumApi::class.java)
} }
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper) private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
var upstream: Upstream? = null var upstream: Upstream<EthereumApi, BlockJson<TransactionRefJson>>? = null
abstract fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
fun <JS, RS> execute(rpcCall: RpcCall<JS, RS>): Mono<ByteArray> { fun <JS, RS> execute(rpcCall: RpcCall<JS, RS>): Mono<ByteArray> {
return execute(0, rpcCall.method, rpcCall.params as List<Any>) 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.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.grpc.Chain 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.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.Disposable import reactor.core.Disposable
@@ -37,9 +39,9 @@ open class EthereumUpstream(
private val options: UpstreamsConfig.Options, private val options: UpstreamsConfig.Options,
val node: QuorumForLabels.QuorumItem, val node: QuorumForLabels.QuorumItem,
private val targets: CallMethods 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()), UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
DirectCallMethods()) DirectCallMethods())
@@ -133,4 +135,18 @@ open class EthereumUpstream(
return targets 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.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.BlockHash
@@ -51,16 +52,16 @@ import java.util.concurrent.atomic.AtomicReference
import java.util.function.Function import java.util.function.Function
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class GrpcUpstream( 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 blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val rpcClient: ReactorEmeraldClient private val rpcClient: ReactorEmeraldClient
): DefaultUpstream(), CachesEnabled, Lifecycle { ) : DefaultUpstream<EthereumApi, BlockJson<TransactionRefJson>>(), CachesEnabled, Lifecycle {
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>() 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 var caches: Caches? = null
private val options = UpstreamsConfig.Options.getDefaults() private val options = UpstreamsConfig.Options.getDefaults()
@@ -214,4 +215,18 @@ open class GrpcUpstream(
this.caches = caches 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 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.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
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.grpc.Chain import io.emeraldpay.grpc.Chain
@@ -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, GrpcUpstream>() private val known = HashMap<Chain, EthereumGrpcUpstream>()
private val lock = ReentrantLock() private val lock = ReentrantLock()
private var grpcTransport: ReactorEmeraldClient? = null private var grpcTransport: ReactorEmeraldClient? = null
@@ -117,7 +117,7 @@ class GrpcUpstreams(
}.map { chainDetails -> }.map { chainDetails ->
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 GrpcUpstream).init(chainDetails) (up.upstream as EthereumGrpcUpstream).init(chainDetails)
up up
} }
@@ -155,10 +155,13 @@ class GrpcUpstreams(
} }
fun getOrCreate(chain: Chain): UpstreamChange { fun getOrCreate(chain: Chain): UpstreamChange {
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
throw IllegalArgumentException("Unsupported blockchain: $chain")
}
lock.withLock { lock.withLock {
val current = known[chain] val current = known[chain]
return if (current == null) { 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 created.timeout = this.timeout
known[chain] = created known[chain] = created
created.start() created.start()
@@ -169,7 +172,7 @@ class GrpcUpstreams(
} }
} }
fun get(chain: Chain): GrpcUpstream { fun get(chain: Chain): EthereumGrpcUpstream {
return known[chain]!! 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.test.UpstreamsMock
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
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.domain.TransactionId import io.infinitape.etherjar.domain.TransactionId
@@ -38,7 +39,7 @@ class StreamHeadSpec extends Specification {
def "Errors on unavailable chain"() { def "Errors on unavailable chain"() {
setup: setup:
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(Upstream)) def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(EthereumUpstream))
def streamHead = new StreamHead(upstreams) def streamHead = new StreamHead(upstreams)
when: when:
def flux = streamHead.add( def flux = streamHead.add(

View File

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

View File

@@ -36,7 +36,7 @@ import spock.lang.Specification
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
class TrackTxSpec extends Specification { class TrackEthereumTxSpec extends Specification {
def chain = Common.ChainRef.CHAIN_ETHEREUM def chain = Common.ChainRef.CHAIN_ETHEREUM
def txId = "0xba61ce4672751fd6086a9ac2b55547a5555af17535b6c0334ede2ecb6d64070a" def txId = "0xba61ce4672751fd6086a9ac2b55547a5555af17535b6c0334ede2ecb6d64070a"
@@ -45,8 +45,8 @@ class TrackTxSpec extends Specification {
def "Gives details for an old transaction"() { def "Gives details for an old transaction"() {
setup: setup:
def req = BlockchainOuterClass.TxStatusRequest.newBuilder() def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
.setChain(chain) .setChain(chain)
.setConfirmationLimit(6) .setConfirmationLimit(6)
.setTxId(txId) .setTxId(txId)
.build() .build()
@@ -91,7 +91,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate()) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init() trackTx.init()
apiMock.answer("eth_getTransactionByHash", [txId], txJson) apiMock.answer("eth_getTransactionByHash", [txId], txJson)
@@ -123,7 +123,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate()) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init() trackTx.init()
apiMock.answer("eth_getTransactionByHash", [txId], null) apiMock.answer("eth_getTransactionByHash", [txId], null)
@@ -180,7 +180,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate()) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init() trackTx.init()
apiMock.answerOnce("eth_getTransactionByHash", [txId], null) apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
@@ -279,7 +279,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate()) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init() trackTx.init()
apiMock.answerOnce("eth_getTransactionByHash", [txId], null) apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
@@ -320,7 +320,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate()) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init() trackTx.init()
def req = BlockchainOuterClass.TxStatusRequest.newBuilder() def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
@@ -341,7 +341,7 @@ class TrackTxSpec extends Specification {
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate()) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
trackTx.init() trackTx.init()
def req = BlockchainOuterClass.TxStatusRequest.newBuilder() 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.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.startup.QuorumForLabels 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.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
@@ -33,11 +33,11 @@ class EthereumUpstreamMock extends EthereumUpstream {
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock() EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) { 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) { 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) { 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.ChainUpstreams
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.JacksonRpcConverter
@@ -75,7 +76,7 @@ class TestingCommons {
} }
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) { 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() { static CachesFactory emptyCaches() {

View File

@@ -18,16 +18,17 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.upstream.AggregatedUpstream import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.ChainUpstreams 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.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
class UpstreamsMock implements Upstreams { class UpstreamsMock implements Upstreams {
private Map<Chain, QuorumBasedMethods> target = [:] private Map<Chain, DefaultEthereumMethods> target = [:]
private Map<Chain, AggregatedUpstream> upstreams = [:] private Map<Chain, AggregatedUpstream> upstreams = [:]
UpstreamsMock(Chain chain, Upstream up) { UpstreamsMock(Chain chain, Upstream up) {
@@ -40,7 +41,7 @@ class UpstreamsMock implements Upstreams {
AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull Upstream up) { AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
if (!upstreams.containsKey(chain)) { 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 { } else {
upstreams[chain].addUpstream(up) upstreams[chain].addUpstream(up)
} }
@@ -63,9 +64,9 @@ class UpstreamsMock implements Upstreams {
} }
@Override @Override
QuorumBasedMethods getDefaultMethods(@NotNull Chain chain) { DefaultEthereumMethods getDefaultMethods(@NotNull Chain chain) {
if (target[chain] == null) { if (target[chain] == null) {
QuorumBasedMethods targets = new QuorumBasedMethods(TestingCommons.objectMapper(), chain) DefaultEthereumMethods targets = new DefaultEthereumMethods(TestingCommons.objectMapper(), chain)
target[chain] = targets target[chain] = targets
} }
return target[chain] 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.test.TestingCommons
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import spock.lang.Specification import spock.lang.Specification
@@ -30,7 +31,7 @@ class AggregatedUpstreamSpec extends Specification {
setup: setup:
def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test1", "eth_test2"])) 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 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: when:
aggr.onUpstreamsUpdated() aggr.onUpstreamsUpdated()
def act = aggr.getMethods() 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.startup.QuorumForLabels
import io.emeraldpay.dshackle.test.EthereumApiStub import io.emeraldpay.dshackle.test.EthereumApiStub
import io.emeraldpay.dshackle.test.TestingCommons 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.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
@@ -35,7 +35,7 @@ class FilteredApisSpec extends Specification {
def rpcClient = Stub(ReactorRpcClient) def rpcClient = Stub(ReactorRpcClient)
def objectMapper = TestingCommons.objectMapper() def objectMapper = TestingCommons.objectMapper()
def ethereumTargets = new QuorumBasedMethods(objectMapper, Chain.ETHEREUM) def ethereumTargets = new DefaultEthereumMethods(objectMapper, Chain.ETHEREUM)
def "Verifies labels"() { def "Verifies labels"() {
setup: setup:

View File

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

View File

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