ethereum as generic upstream (#331)

- merge all Ethereum implementations to Generic upstreams and multistreams
- merge evm-pos and pow to ethereum
This commit is contained in:
a10zn8
2023-10-31 16:28:15 +03:00
committed by GitHub
parent 4ea4fa5d81
commit 08cbffe7d1
69 changed files with 826 additions and 2790 deletions

View File

@@ -33,7 +33,10 @@ data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<Chains
) { ) {
companion object { companion object {
@JvmStatic @JvmStatic
fun default() = ChainConfig( fun default() = defaultWithContract(null)
@JvmStatic
fun defaultWithContract(callLimitContract: String?) = ChainConfig(
Duration.ofSeconds(12), Duration.ofSeconds(12),
6, 6,
1, 1,
@@ -43,11 +46,14 @@ data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<Chains
0, 0,
"UNKNOWN", "UNKNOWN",
emptyList(), emptyList(),
null, callLimitContract,
"undefined", "undefined",
"undefined", "undefined",
) )
} }
} }
fun resolve(chain: String): ChainConfig { fun resolve(chain: String): ChainConfig {

View File

@@ -1,27 +1,21 @@
package io.emeraldpay.dshackle package io.emeraldpay.dshackle
enum class BlockchainType { enum class BlockchainType {
BITCOIN, EVM_POW, EVM_POS, STARKNET; BITCOIN, ETHEREUM, STARKNET;
companion object { companion object {
val pow = setOf(
Chain.ETHEREUM_CLASSIC__MAINNET,
)
val bitcoin = setOf(Chain.BITCOIN__MAINNET, Chain.BITCOIN__TESTNET) val bitcoin = setOf(Chain.BITCOIN__MAINNET, Chain.BITCOIN__TESTNET)
val starknet = setOf(Chain.STARKNET__MAINNET, Chain.STARKNET__TESTNET, Chain.STARKNET__TESTNET_2) val starknet = setOf(Chain.STARKNET__MAINNET, Chain.STARKNET__TESTNET, Chain.STARKNET__TESTNET_2)
@JvmStatic @JvmStatic
fun from(chain: Chain): BlockchainType { fun from(chain: Chain): BlockchainType {
return if (pow.contains(chain)) { return if (bitcoin.contains(chain)) {
EVM_POW
} else if (bitcoin.contains(chain)) {
BITCOIN BITCOIN
} else if (starknet.contains(chain)) { } else if (starknet.contains(chain)) {
STARKNET STARKNET
} else { } else {
EVM_POS ETHEREUM
} }
} }
} }

View File

@@ -97,7 +97,6 @@ open class CachesFactory(
caches.setReceipts(ReceiptRedisCache(redis.reactive(), chain)) caches.setReceipts(ReceiptRedisCache(redis.reactive(), chain))
caches.setHeightByHash(HeightByHashRedisCache(redis.reactive(), chain)) caches.setHeightByHash(HeightByHashRedisCache(redis.reactive(), chain))
} }
caches.setCacheEnabled(cacheConfig.requestsCacheEnabled)
return caches.build() return caches.build()
} }

View File

@@ -17,8 +17,6 @@ package io.emeraldpay.dshackle.config
class CacheConfig { class CacheConfig {
var requestsCacheEnabled = true
var redis: Redis? = null var redis: Redis? = null
class Redis( class Redis(

View File

@@ -16,21 +16,13 @@
package io.emeraldpay.dshackle.config package io.emeraldpay.dshackle.config
import io.emeraldpay.dshackle.foundation.YamlConfigReader import io.emeraldpay.dshackle.foundation.YamlConfigReader
import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.nodes.MappingNode import org.yaml.snakeyaml.nodes.MappingNode
class CacheConfigReader : YamlConfigReader<CacheConfig>() { class CacheConfigReader : YamlConfigReader<CacheConfig>() {
companion object {
private val log = LoggerFactory.getLogger(CacheConfigReader::class.java)
}
override fun read(input: MappingNode?): CacheConfig? { override fun read(input: MappingNode?): CacheConfig? {
return getMapping(input, "cache")?.let { node -> return getMapping(input, "cache")?.let { node ->
val config = CacheConfig() val config = CacheConfig()
getValueAsBool(node, "requests-cache-enabled")?.let {
config.requestsCacheEnabled = it
}
getMapping(node, "redis")?.let { redisNode -> getMapping(node, "redis")?.let { redisNode ->
val redis = CacheConfig.Redis() val redis = CacheConfig.Redis()
val enabled = getValueAsBool(redisNode, "enabled") ?: true val enabled = getValueAsBool(redisNode, "enabled") ?: true
@@ -50,7 +42,7 @@ class CacheConfigReader : YamlConfigReader<CacheConfig>() {
config.redis = redis config.redis = redis
} }
} }
if (config.redis == null && config.requestsCacheEnabled) { if (config.redis == null) {
return null return null
} }
config config

View File

@@ -41,7 +41,7 @@ class TokensConfig(
type == null -> type type == null -> type
address.isNullOrBlank() -> "address" address.isNullOrBlank() -> "address"
blockchain != null && blockchain != null &&
(BlockchainType.from(blockchain!!) == BlockchainType.EVM_POS || BlockchainType.from(blockchain!!) == BlockchainType.EVM_POW) && (BlockchainType.from(blockchain!!) == BlockchainType.ETHEREUM) &&
!Address.isValidAddress(address) -> "address" !Address.isValidAddress(address) -> "address"
else -> null else -> null
} }

View File

@@ -2,16 +2,12 @@ package io.emeraldpay.dshackle.config.context
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.BlockchainType.EVM_POS
import io.emeraldpay.dshackle.BlockchainType.EVM_POW
import io.emeraldpay.dshackle.BlockchainType.STARKNET
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.upstream.CallTargetsHolder import io.emeraldpay.dshackle.upstream.CallTargetsHolder
import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.dshackle.upstream.generic.ChainSpecificRegistry
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.generic.GenericMultistream import io.emeraldpay.dshackle.upstream.generic.GenericMultistream
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
@@ -31,14 +27,13 @@ open class MultistreamsConfig(val beanFactory: ConfigurableListableBeanFactory)
headScheduler: Scheduler, headScheduler: Scheduler,
tracer: Tracer, tracer: Tracer,
): List<Multistream> { ): List<Multistream> {
return Chain.values() return Chain.entries
.filterNot { it == Chain.UNSPECIFIED } .filterNot { it == Chain.UNSPECIFIED }
.map { chain -> .map { chain ->
when (BlockchainType.from(chain)) { if (BlockchainType.from(chain) == BITCOIN) {
EVM_POS -> ethereumPosMultistream(chain, cachesFactory, headScheduler, tracer) bitcoinMultistream(chain, cachesFactory, headScheduler)
EVM_POW -> ethereumMultistream(chain, cachesFactory, headScheduler, tracer) } else {
BITCOIN -> bitcoinMultistream(chain, cachesFactory, headScheduler) genericMultistream(chain, cachesFactory, headScheduler, tracer)
STARKNET -> genericMultistream(chain, cachesFactory, headScheduler)
} }
} }
} }
@@ -47,47 +42,18 @@ open class MultistreamsConfig(val beanFactory: ConfigurableListableBeanFactory)
chain: Chain, chain: Chain,
cachesFactory: CachesFactory, cachesFactory: CachesFactory,
headScheduler: Scheduler, headScheduler: Scheduler,
tracer: Tracer,
): Multistream { ): Multistream {
val name = "multi-$chain" val name = "multi-$chain"
val cs = ChainSpecificRegistry.resolve(chain)
return GenericMultistream( return GenericMultistream(
chain, chain,
CopyOnWriteArrayList(), CopyOnWriteArrayList(),
cachesFactory.getCaches(chain), cachesFactory.getCaches(chain),
headScheduler, headScheduler,
).also { register(it, name) } cs.makeCachingReaderBuilder(tracer),
} cs::localReaderBuilder,
cs.subscriptionBuilder(headScheduler),
private fun ethereumMultistream(
chain: Chain,
cachesFactory: CachesFactory,
headScheduler: Scheduler,
tracer: Tracer,
): EthereumMultistream {
val name = "multi-ethereum-$chain"
return EthereumMultistream(
chain,
CopyOnWriteArrayList(),
cachesFactory.getCaches(chain),
headScheduler,
tracer,
).also { register(it, name) }
}
open fun ethereumPosMultistream(
chain: Chain,
cachesFactory: CachesFactory,
headScheduler: Scheduler,
tracer: Tracer,
): EthereumPosMultiStream {
val name = "multi-ethereum-pos-$chain"
return EthereumPosMultiStream(
chain,
CopyOnWriteArrayList(),
cachesFactory.getCaches(chain),
headScheduler,
tracer,
).also { register(it, name) } ).also { register(it, name) }
} }

View File

@@ -20,7 +20,7 @@ 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.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.EVM_POS import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.Global.Companion.nullValue import io.emeraldpay.dshackle.Global.Companion.nullValue
@@ -76,7 +76,6 @@ open class NativeCall(
private val log = LoggerFactory.getLogger(NativeCall::class.java) private val log = LoggerFactory.getLogger(NativeCall::class.java)
private val objectMapper: ObjectMapper = Global.objectMapper private val objectMapper: ObjectMapper = Global.objectMapper
private val localRouterEnabled = config.cache?.requestsCacheEnabled ?: true
private val passthrough = config.passthrough private val passthrough = config.passthrough
var rpcReaderFactory: RpcReaderFactory = RpcReaderFactory.default() var rpcReaderFactory: RpcReaderFactory = RpcReaderFactory.default()
@@ -85,7 +84,7 @@ open class NativeCall(
@EventListener @EventListener
fun onUpstreamChangeEvent(event: UpstreamChangeEvent) { fun onUpstreamChangeEvent(event: UpstreamChangeEvent) {
multistreamHolder.getUpstream(event.chain).let { up -> multistreamHolder.getUpstream(event.chain).let { up ->
if (BlockchainType.from(up.chain) == EVM_POS) { if (BlockchainType.from(up.chain) == ETHEREUM) {
ethereumCallSelectors.putIfAbsent( ethereumCallSelectors.putIfAbsent(
event.chain, event.chain,
EthereumCallSelector(up.caches), EthereumCallSelector(up.caches),
@@ -307,7 +306,7 @@ open class NativeCall(
} }
// for ethereum the actual block needed for the call may be specified in the call parameters // for ethereum the actual block needed for the call may be specified in the call parameters
val callSpecificMatcher: Mono<Selector.Matcher> = val callSpecificMatcher: Mono<Selector.Matcher> =
if (BlockchainType.from(upstream.chain) == BlockchainType.EVM_POS || BlockchainType.from(upstream.chain) == BlockchainType.EVM_POW) { if (BlockchainType.from(upstream.chain) == ETHEREUM) {
ethereumCallSelectors[chain]?.getMatcher(method, params, upstream.getHead(), passthrough) ethereumCallSelectors[chain]?.getMatcher(method, params, upstream.getHead(), passthrough)
} else { } else {
null null
@@ -359,7 +358,7 @@ open class NativeCall(
if (method in DefaultEthereumMethods.newFilterMethods) CreateFilterDecorator() else NoneResultDecorator() if (method in DefaultEthereumMethods.newFilterMethods) CreateFilterDecorator() else NoneResultDecorator()
fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> { fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> {
return ctx.upstream.getLocalReader(localRouterEnabled) return ctx.upstream.getLocalReader()
.flatMap { api -> .flatMap { api ->
SpannedReader(api, tracer, LOCAL_READER) SpannedReader(api, tracer, LOCAL_READER)
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce, ctx.forwardedSelector)) .read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce, ctx.forwardedSelector))

View File

@@ -57,7 +57,7 @@ open class NativeSubscribe(
fun start(request: BlockchainOuterClass.NativeSubscribeRequest): Publisher<ResponseHolder> { fun start(request: BlockchainOuterClass.NativeSubscribeRequest): Publisher<ResponseHolder> {
val chain = Chain.byId(request.chainValue) val chain = Chain.byId(request.chainValue)
if (BlockchainType.from(chain) != BlockchainType.EVM_POS && BlockchainType.from(chain) != BlockchainType.EVM_POW) { if (BlockchainType.from(chain) != BlockchainType.ETHEREUM) {
return Mono.error(UnsupportedOperationException("Native subscribe is not supported for ${chain.chainCode}")) return Mono.error(UnsupportedOperationException("Native subscribe is not supported for ${chain.chainCode}"))
} }

View File

@@ -20,9 +20,7 @@ import brave.grpc.GrpcTracing
import com.google.common.annotations.VisibleForTesting import com.google.common.annotations.VisibleForTesting
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.BlockchainType.EVM_POS import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
import io.emeraldpay.dshackle.BlockchainType.EVM_POW
import io.emeraldpay.dshackle.BlockchainType.STARKNET
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.FileResolver
@@ -52,13 +50,12 @@ import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
import io.emeraldpay.dshackle.upstream.bitcoin.ZMQServer import io.emeraldpay.dshackle.upstream.bitcoin.ZMQServer
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumBlockValidator
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeRpcUpstream
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionFactory import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionFactory
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPoolFactory import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPoolFactory
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
import io.emeraldpay.dshackle.upstream.generic.ChainSpecificRegistry
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory.ConnectorMode.RPC_REQUESTS_WITH_MIXED_HEAD import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory.ConnectorMode.RPC_REQUESTS_WITH_MIXED_HEAD
@@ -75,6 +72,7 @@ import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import reactor.core.scheduler.Scheduler import reactor.core.scheduler.Scheduler
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.lang.IllegalStateException
import java.net.URI import java.net.URI
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -138,24 +136,6 @@ open class ConfiguredUpstreams(
.merge(up.options ?: ChainOptions.PartialOptions()) .merge(up.options ?: ChainOptions.PartialOptions())
.buildOptions() .buildOptions()
val upstream = when (BlockchainType.from(chain)) { val upstream = when (BlockchainType.from(chain)) {
EVM_POS -> {
buildEthereumPosUpstream(
up.nodeId,
up.cast(EthereumPosConnection::class.java),
chain,
options,
chainConfig,
)
}
EVM_POW -> {
buildEthereumUpstream(
up.nodeId,
up.cast(RpcConnection::class.java),
chain,
options,
chainConfig,
)
}
BITCOIN -> { BITCOIN -> {
buildBitcoinUpstream( buildBitcoinUpstream(
up.cast(BitcoinConnection::class.java), up.cast(BitcoinConnection::class.java),
@@ -165,13 +145,29 @@ open class ConfiguredUpstreams(
) )
} }
STARKNET -> { ETHEREUM -> {
buildStarknetUpstream( val posConn = up.cast(EthereumPosConnection::class.java)
buildGenericUpstream(
up.nodeId, up.nodeId,
up.cast(RpcConnection::class.java), up,
posConn.connection?.execution ?: throw IllegalStateException("Empty execution config"),
chain, chain,
options, options,
chainConfig, chainConfig,
posConn.connection?.upstreamRating ?: 0,
)
}
else -> {
buildGenericUpstream(
up.nodeId,
up,
up.connection as RpcConnection,
chain,
options,
chainConfig,
0,
) )
} }
} }
@@ -224,31 +220,30 @@ open class ConfiguredUpstreams(
} }
} }
private fun buildStarknetUpstream( private fun buildGenericUpstream(
nodeId: Int?, nodeId: Int?,
config: UpstreamsConfig.Upstream<RpcConnection>, config: UpstreamsConfig.Upstream<*>,
connection: RpcConnection,
chain: Chain, chain: Chain,
options: Options, options: Options,
chainConfig: ChainConfig, chainConfig: ChainConfig,
nodeRating: Int,
): Upstream? { ): Upstream? {
if (config.connection == null) { if (config.connection == null) {
log.warn("Upstream doesn't have connection configuration") log.warn("Upstream doesn't have connection configuration")
return null return null
} }
val connection = config.connection!! val cs = ChainSpecificRegistry.resolve(chain)
val connectorFactory = buildConnectorFactory( val connectorFactory = buildConnectorFactory(
config.id!!, config.id!!,
connection, connection,
chain, chain,
NoChoiceWithPriorityForkChoice(0, config.id!!), NoChoiceWithPriorityForkChoice(nodeRating, config.id!!),
BlockValidator.ALWAYS_VALID, BlockValidator.ALWAYS_VALID,
chainConfig, chainConfig,
) ) ?: return null
if (connectorFactory == null) {
return null
}
val methods = buildMethods(config, chain) val methods = buildMethods(config, chain)
@@ -268,6 +263,9 @@ open class ConfiguredUpstreams(
chainConfig, chainConfig,
connectorFactory, connectorFactory,
eventPublisher, eventPublisher,
cs::validator,
cs::labelDetector,
cs::subscriptionTopics,
) )
upstream.start() upstream.start()
@@ -278,58 +276,6 @@ open class ConfiguredUpstreams(
return upstream return upstream
} }
private fun buildEthereumPosUpstream(
nodeId: Int?,
config: UpstreamsConfig.Upstream<EthereumPosConnection>,
chain: Chain,
options: Options,
chainConf: ChainConfig,
): Upstream? {
val conn = config.connection!!
val execution = conn.execution
if (execution == null) {
log.warn("Upstream doesn't have execution layer configuration")
return null
}
val urls = ArrayList<URI>()
val connectorFactory = buildConnectorFactory(
config.id!!,
execution,
chain,
NoChoiceWithPriorityForkChoice(conn.upstreamRating, config.id!!),
BlockValidator.ALWAYS_VALID,
chainConf,
)
val methods = buildMethods(config, chain)
if (connectorFactory == null) {
return null
}
val hashUrl = conn.execution!!.let {
if (it.connectorMode == RPC_REQUESTS_WITH_MIXED_HEAD.name) it.rpc?.url ?: it.ws?.url else it.ws?.url ?: it.rpc?.url
}
val hash = getHash(nodeId, hashUrl!!)
val upstream = EthereumLikeRpcUpstream(
config.id!!,
hash,
chain,
options,
config.role,
methods,
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(config.labels)),
connectorFactory,
chainConf,
true,
eventPublisher,
)
upstream.start()
if (!upstream.isRunning) {
log.debug("Upstream ${upstream.getId()} is not running, it can't be added")
return null
}
return upstream
}
private fun buildBitcoinUpstream( private fun buildBitcoinUpstream(
config: UpstreamsConfig.Upstream<BitcoinConnection>, config: UpstreamsConfig.Upstream<BitcoinConnection>,
chain: Chain, chain: Chain,
@@ -373,45 +319,6 @@ open class ConfiguredUpstreams(
return upstream return upstream
} }
private fun buildEthereumUpstream(
nodeId: Int?,
config: UpstreamsConfig.Upstream<RpcConnection>,
chain: Chain,
options: Options,
chainConf: ChainConfig,
): Upstream? {
val conn = config.connection!!
val methods = buildMethods(config, chain)
val connectorFactory = buildConnectorFactory(
config.id!!,
conn,
chain,
MostWorkForkChoice(),
EthereumBlockValidator(),
chainConf,
)
if (connectorFactory == null) {
return null
}
val hashUrl = if (conn.connectorMode == RPC_REQUESTS_WITH_MIXED_HEAD.name) conn.rpc?.url ?: conn.ws?.url else conn.ws?.url ?: conn.rpc?.url
val upstream = EthereumLikeRpcUpstream(
config.id!!,
getHash(nodeId, hashUrl!!),
chain,
options, config.role,
methods,
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(config.labels)),
connectorFactory,
chainConf,
false,
eventPublisher,
)
upstream.start()
return upstream
}
private fun buildGrpcUpstream( private fun buildGrpcUpstream(
nodeId: Int?, nodeId: Int?,
config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>,
@@ -460,13 +367,13 @@ open class ConfiguredUpstreams(
private fun buildHttpFactory(conn: HttpEndpoint?, urls: ArrayList<URI>? = null): HttpRpcFactory? { private fun buildHttpFactory(conn: HttpEndpoint?, urls: ArrayList<URI>? = null): HttpRpcFactory? {
return conn?.let { endpoint -> return conn?.let { endpoint ->
val tls = conn?.tls?.let { tls -> val tls = conn.tls?.let { tls ->
tls.ca?.let { ca -> tls.ca?.let { ca ->
fileResolver.resolve(ca).readBytes() fileResolver.resolve(ca).readBytes()
} }
} }
urls?.add(endpoint.url) urls?.add(endpoint.url)
HttpRpcFactory(endpoint.url.toString(), conn?.basicAuth, tls) HttpRpcFactory(endpoint.url.toString(), conn.basicAuth, tls)
} }
} }

View File

@@ -1,131 +0,0 @@
package io.emeraldpay.dshackle.upstream
import com.github.benmanes.caffeine.cache.Caffeine
import io.emeraldpay.api.proto.BlockchainOuterClass
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
import java.util.EnumMap
import java.util.function.Function
abstract class AbstractChainFees<F, B, TR, T>(
private val heightLimit: Int,
private val upstreams: Multistream,
extractTx: (B) -> List<TR>?,
) : ChainFees {
companion object {
private val log = LoggerFactory.getLogger(AbstractChainFees::class.java)
}
private val txSource = EnumMap<ChainFees.Mode, TxAt<B, TR>>(ChainFees.Mode::class.java)
init {
txSource[ChainFees.Mode.AVG_TOP] = TxAtTop(extractTx)
txSource[ChainFees.Mode.MIN_ALWAYS] = TxAtBottom(extractTx)
txSource[ChainFees.Mode.AVG_MIDDLE] = TxAtMiddle(extractTx)
txSource[ChainFees.Mode.AVG_LAST] = TxAtBottom(extractTx)
txSource[ChainFees.Mode.AVG_T5] = TxAtPos(extractTx, 5)
txSource[ChainFees.Mode.AVG_T20] = TxAtPos(extractTx, 20)
txSource[ChainFees.Mode.AVG_T50] = TxAtPos(extractTx, 50)
}
override fun estimate(mode: ChainFees.Mode, blocks: Int): Mono<BlockchainOuterClass.EstimateFeeResponse> {
return usingBlocks(blocks)
.flatMap { readFeesAt(it, mode) }
.transform(feeAggregation(mode))
.next()
.map(getResponseBuilder())
}
// ---
private val feeCache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(60))
.build<Pair<Long, ChainFees.Mode>, F>()
fun usingBlocks(exp: Int): Flux<Long> {
val useBlocks = exp.coerceAtMost(heightLimit).coerceAtLeast(1)
val height = upstreams.getHead().getCurrentHeight()
?: return Mono.fromCallable { log.warn("Upstream is not ready. No current height") }.thenMany(Mono.empty()) // TODO or throw an exception to build a gRPC error?
val startBlock: Int = height.toInt() - useBlocks + 1
if (startBlock < 0) {
log.warn("Blockchain doesn't have enough blocks. Height: $height")
return Flux.empty()
}
return Flux.range(startBlock, useBlocks).map { it.toLong() }
}
fun readFeesAt(height: Long, mode: ChainFees.Mode): Mono<F> {
val current = feeCache.getIfPresent(Pair(height, mode))
if (current != null) {
return Mono.just(current)
}
val txSelector = txSourceFor(mode)
return readFeesAt(height, txSelector).doOnNext {
// TODO it may be EMPTY for some blocks (ex. a no tx block), so nothing gets cached and goes to do the same call each time. so do cache empty values to avoid useless requests
feeCache.put(Pair(height, mode), it!!)
}
}
open fun txSourceFor(mode: ChainFees.Mode): TxAt<B, TR> {
return txSource[mode] ?: throw IllegalStateException("No TS Source for mode $mode")
}
abstract fun readFeesAt(height: Long, selector: TxAt<B, TR>): Mono<F>
abstract fun feeAggregation(mode: ChainFees.Mode): Function<Flux<F>, Mono<F>>
abstract fun getResponseBuilder(): Function<F, BlockchainOuterClass.EstimateFeeResponse>
abstract class TxAt<B, TR>(private val extractTx: Function<B, List<TR>?>) {
fun get(block: B): TR? {
val txes = extractTx.apply(block) ?: return null
return get(txes)
}
abstract fun get(transactions: List<TR>): TR?
}
class TxAtPos<B, TR>(extractTx: Function<B, List<TR>?>, private val pos: Int) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
val index = pos.coerceAtMost(transactions.size - 1)
if (index < 0) {
return null
}
return transactions[transactions.size - index - 1]
}
}
class TxAtTop<B, TR>(extractTx: Function<B, List<TR>?>) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
if (transactions.isEmpty()) {
return null
}
return transactions[0]
}
}
class TxAtBottom<B, TR>(extractTx: Function<B, List<TR>?>) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
if (transactions.isEmpty()) {
return null
}
return transactions.last()
}
}
class TxAtMiddle<B, TR>(extractTx: Function<B, List<TR>?>) : TxAt<B, TR>(extractTx) {
override fun get(transactions: List<TR>): TR? {
if (transactions.isEmpty()) {
return null
}
if (transactions.size == 1) {
return transactions[0]
}
return transactions[transactions.size / 2]
}
}
}

View File

@@ -1,3 +1,15 @@
package io.emeraldpay.dshackle.upstream package io.emeraldpay.dshackle.upstream
interface CachingReader interface CachingReader : Lifecycle
object NoopCachingReader : CachingReader {
override fun start() {
}
override fun stop() {
}
override fun isRunning(): Boolean {
return true
}
}

View File

@@ -18,9 +18,8 @@ class CallTargetsHolder {
private fun setupDefaultMethods(chain: Chain): CallMethods { private fun setupDefaultMethods(chain: Chain): CallMethods {
val created = when (BlockchainType.from(chain)) { val created = when (BlockchainType.from(chain)) {
BlockchainType.EVM_POW -> DefaultEthereumMethods(chain)
BlockchainType.BITCOIN -> DefaultBitcoinMethods() BlockchainType.BITCOIN -> DefaultBitcoinMethods()
BlockchainType.EVM_POS -> DefaultEthereumMethods(chain) BlockchainType.ETHEREUM -> DefaultEthereumMethods(chain)
BlockchainType.STARKNET -> DefaultStarknetMethods(chain) BlockchainType.STARKNET -> DefaultStarknetMethods(chain)
} }
callTargets[chain] = created callTargets[chain] = created

View File

@@ -1,35 +0,0 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.api.proto.BlockchainOuterClass
import reactor.core.publisher.Mono
interface ChainFees {
companion object {
fun extractMode(req: BlockchainOuterClass.EstimateFeeRequest): Mode? {
return when (req.mode!!) {
BlockchainOuterClass.FeeEstimationMode.INVALID -> null
BlockchainOuterClass.FeeEstimationMode.AVG_LAST -> Mode.AVG_LAST
BlockchainOuterClass.FeeEstimationMode.AVG_T5 -> Mode.AVG_T5
BlockchainOuterClass.FeeEstimationMode.AVG_T20 -> Mode.AVG_T20
BlockchainOuterClass.FeeEstimationMode.AVG_T50 -> Mode.AVG_T50
BlockchainOuterClass.FeeEstimationMode.MIN_ALWAYS -> Mode.MIN_ALWAYS
BlockchainOuterClass.FeeEstimationMode.AVG_MIDDLE -> Mode.AVG_MIDDLE
BlockchainOuterClass.FeeEstimationMode.AVG_TOP -> Mode.AVG_TOP
BlockchainOuterClass.FeeEstimationMode.UNRECOGNIZED -> null
}
}
}
fun estimate(mode: Mode, blocks: Int): Mono<BlockchainOuterClass.EstimateFeeResponse>
enum class Mode {
AVG_LAST,
AVG_T5,
AVG_T20,
AVG_T50,
MIN_ALWAYS,
AVG_MIDDLE,
AVG_TOP,
}
}

View File

@@ -22,7 +22,7 @@ interface EgressSubscription {
fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher): Flux<out Any>
} }
class EmptyEgressSubscription : EgressSubscription { object EmptyEgressSubscription : EgressSubscription {
override fun getAvailableTopics(): List<String> { override fun getAvailableTopics(): List<String> {
return emptyList() return emptyList()
} }

View File

@@ -0,0 +1,10 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.reader.JsonRpcReader
import reactor.core.publisher.Flux
typealias LabelsDetectorBuilder = (Chain, JsonRpcReader) -> LabelsDetector?
interface LabelsDetector {
fun detectLabels(): Flux<Pair<String, String>>
}

View File

@@ -51,10 +51,12 @@ import kotlin.concurrent.withLock
*/ */
abstract class Multistream( abstract class Multistream(
val chain: Chain, val chain: Chain,
private val upstreams: MutableList<Upstream>,
val caches: Caches, val caches: Caches,
) : Upstream, Lifecycle, HasEgressSubscription { ) : Upstream, Lifecycle, HasEgressSubscription {
abstract fun getUpstreams(): MutableList<out Upstream>
abstract fun addUpstreamInternal(u: Upstream)
companion object { companion object {
private const val metrics = "upstreams" private const val metrics = "upstreams"
} }
@@ -93,30 +95,6 @@ abstract class Multistream(
.multicast() .multicast()
.directBestEffort<Upstream>() .directBestEffort<Upstream>()
init {
UpstreamAvailability.values().forEach { status ->
Metrics.gauge(
"$metrics.availability",
listOf(Tag.of("chain", chain.chainCode), Tag.of("status", status.name.lowercase())),
this,
) {
upstreams.count { it.getStatus() == status }.toDouble()
}
}
Metrics.gauge(
"$metrics.connected",
listOf(Tag.of("chain", chain.chainCode)),
this,
) {
upstreams.size.toDouble()
}
upstreams.forEach { up ->
monitorUpstream(up)
}
}
override fun getSubscriptionTopics(): List<String> { override fun getSubscriptionTopics(): List<String> {
return getEgressSubscription().getAvailableTopics() return getEgressSubscription().getAvailableTopics()
} }
@@ -153,29 +131,49 @@ abstract class Multistream(
onUpstreamsUpdated() onUpstreamsUpdated()
} }
init {
UpstreamAvailability.entries.forEach { status ->
Metrics.gauge(
"$metrics.availability",
listOf(Tag.of("chain", chain.chainCode), Tag.of("status", status.name.lowercase())),
this,
) {
getAll().count { it.getStatus() == status }.toDouble()
}
}
Metrics.gauge(
"$metrics.connected",
listOf(Tag.of("chain", chain.chainCode)),
this,
) {
getAll().size.toDouble()
}
}
/** /**
* Get list of all underlying upstreams * Get list of all underlying upstreams
*/ */
open fun getAll(): List<Upstream> { open fun getAll(): List<Upstream> {
return upstreams return getUpstreams()
} }
/** /**
* Add an upstream * Add an upstream
*/ */
fun addUpstream(upstream: Upstream): Boolean = fun addUpstream(upstream: Upstream): Boolean =
upstreams.none { getUpstreams().none {
it.getId() == upstream.getId() it.getId() == upstream.getId()
}.also { }.also {
if (it) { if (it) {
upstreams.add(upstream) addUpstreamInternal(upstream)
addHead(upstream) addHead(upstream)
monitorUpstream(upstream) monitorUpstream(upstream)
} }
} }
fun removeUpstream(id: String): Boolean = fun removeUpstream(id: String): Boolean =
upstreams.removeIf { up -> getUpstreams().removeIf { up ->
(up.getId() == id).also { (up.getId() == id).also {
if (it) { if (it) {
up.stop() up.stop()
@@ -196,13 +194,13 @@ abstract class Multistream(
if (seq >= Int.MAX_VALUE / 2) { if (seq >= Int.MAX_VALUE / 2) {
seq = 0 seq = 0
} }
return FilteredApis(chain, upstreams, matcher, i) return FilteredApis(chain, getUpstreams(), matcher, i)
} }
/** /**
* Finds an API that leverages caches and other optimizations/transformations of the request. * Finds an API that leverages caches and other optimizations/transformations of the request.
*/ */
abstract fun getLocalReader(localEnabled: Boolean): Mono<JsonRpcReader> abstract fun getLocalReader(): Mono<JsonRpcReader>
override fun getIngressReader(): JsonRpcReader { override fun getIngressReader(): JsonRpcReader {
throw NotImplementedError("Immediate direct API is not implemented for Aggregated Upstream") throw NotImplementedError("Immediate direct API is not implemented for Aggregated Upstream")
@@ -235,6 +233,7 @@ abstract class Multistream(
lagObserver = null lagObserver = null
upstreams[0].setLag(0) upstreams[0].setLag(0)
} }
upstreams.size > 1 -> if (lagObserver == null) lagObserver = makeLagObserver() upstreams.size > 1 -> if (lagObserver == null) lagObserver = makeLagObserver()
} }
} }
@@ -389,17 +388,17 @@ abstract class Multistream(
} catch (e: Exception) { } catch (e: Exception) {
log.warn("Head processing error: ${e.javaClass} ${e.message}") log.warn("Head processing error: ${e.javaClass} ${e.message}")
} }
val statuses = upstreams.asSequence().map { it.getStatus() } val statuses = getUpstreams().asSequence().map { it.getStatus() }
.groupBy { it } .groupBy { it }
.map { "${it.key.name}/${it.value.size}" } .map { "${it.key.name}/${it.value.size}" }
.joinToString(",") .joinToString(",")
val lag = upstreams.joinToString(", ") { val lag = getUpstreams().joinToString(", ") {
// by default, when no lag is available it uses Long.MAX_VALUE, and it doesn't make sense to print // by default, when no lag is available it uses Long.MAX_VALUE, and it doesn't make sense to print
// status with such value. use NA (as Not Available) instead // status with such value. use NA (as Not Available) instead
val value = it.getLag() val value = it.getLag()
value?.toString() ?: "NA" value?.toString() ?: "NA"
} }
val weak = upstreams val weak = getUpstreams()
.filter { it.getStatus() != UpstreamAvailability.OK } .filter { it.getStatus() != UpstreamAvailability.OK }
.joinToString(", ") { it.getId() } .joinToString(", ") { it.getId() }
@@ -424,6 +423,7 @@ abstract class Multistream(
onUpstreamsUpdated() onUpstreamsUpdated()
updateUpstreams.emitNext(event.upstream) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED } updateUpstreams.emitNext(event.upstream) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
} }
UpstreamChangeEvent.ChangeType.ADDED -> { UpstreamChangeEvent.ChangeType.ADDED -> {
if (!started) { if (!started) {
start() start()
@@ -441,6 +441,7 @@ abstract class Multistream(
} }
} }
} }
UpstreamChangeEvent.ChangeType.REMOVED -> { UpstreamChangeEvent.ChangeType.REMOVED -> {
removeUpstream(event.upstream.getId()).takeIf { it }?.let { removeUpstream(event.upstream.getId()).takeIf { it }?.let {
try { try {
@@ -458,10 +459,10 @@ abstract class Multistream(
} }
fun haveUpstreams(): Boolean = fun haveUpstreams(): Boolean =
upstreams.isNotEmpty() getUpstreams().isNotEmpty()
fun hasMatchingUpstream(matcher: Selector.LabelSelectorMatcher): Boolean { fun hasMatchingUpstream(matcher: Selector.LabelSelectorMatcher): Boolean {
return upstreams.any { matcher.matches(it) } return getUpstreams().any { matcher.matches(it) }
} }
fun subscribeAddedUpstreams(): Flux<Upstream> = fun subscribeAddedUpstreams(): Flux<Upstream> =
@@ -469,12 +470,16 @@ abstract class Multistream(
fun subscribeRemovedUpstreams(): Flux<Upstream> = fun subscribeRemovedUpstreams(): Flux<Upstream> =
removedUpstreams.asFlux() removedUpstreams.asFlux()
fun subscribeUpdatedUpstreams(): Flux<Upstream> = fun subscribeUpdatedUpstreams(): Flux<Upstream> =
updateUpstreams.asFlux() updateUpstreams.asFlux()
abstract fun makeLagObserver(): HeadLagObserver abstract fun makeLagObserver(): HeadLagObserver
open fun tryProxySubscribe(matcher: Selector.Matcher, request: BlockchainOuterClass.NativeSubscribeRequest): Flux<out Any>? = null open fun tryProxySubscribe(
matcher: Selector.Matcher,
request: BlockchainOuterClass.NativeSubscribeRequest,
): Flux<out Any>? = null
abstract fun getCachingReader(): CachingReader? abstract fun getCachingReader(): CachingReader?

View File

@@ -0,0 +1,25 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.foundation.ChainOptions
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
typealias UpstreamValidatorBuilder = (Chain, Upstream, ChainOptions.Options, ChainConfig) -> UpstreamValidator?
interface UpstreamValidator {
fun start(): Flux<UpstreamAvailability>
fun validateUpstreamSettings(): Mono<ValidateUpstreamSettingsResult>
fun validateUpstreamSettingsOnStartup(): ValidateUpstreamSettingsResult {
return validateUpstreamSettings().block() ?: ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR
}
}
enum class ValidateUpstreamSettingsResult {
UPSTREAM_VALID,
UPSTREAM_SETTINGS_ERROR,
UPSTREAM_FATAL_SETTINGS_ERROR,
}

View File

@@ -43,7 +43,7 @@ open class BitcoinMultistream(
private val sourceUpstreams: MutableList<BitcoinUpstream>, private val sourceUpstreams: MutableList<BitcoinUpstream>,
caches: Caches, caches: Caches,
private val headScheduler: Scheduler, private val headScheduler: Scheduler,
) : Multistream(chain, sourceUpstreams as MutableList<Upstream>, caches), Lifecycle { ) : Multistream(chain, caches), Lifecycle {
private var head: Head = EmptyHead() private var head: Head = EmptyHead()
private var esplora = sourceUpstreams.find { it.esploraClient != null }?.esploraClient private var esplora = sourceUpstreams.find { it.esploraClient != null }?.esploraClient
@@ -51,6 +51,13 @@ open class BitcoinMultistream(
private var addressActiveCheck: AddressActiveCheck? = null private var addressActiveCheck: AddressActiveCheck? = null
private var xpubAddresses: XpubAddresses? = null private var xpubAddresses: XpubAddresses? = null
private var callRouter: LocalCallRouter = LocalCallRouter(DefaultBitcoinMethods(), reader) private var callRouter: LocalCallRouter = LocalCallRouter(DefaultBitcoinMethods(), reader)
override fun getUpstreams(): MutableList<out Upstream> {
return sourceUpstreams
}
override fun addUpstreamInternal(u: Upstream) {
sourceUpstreams.add(u as BitcoinUpstream)
}
override fun init() { override fun init() {
if (sourceUpstreams.size > 0) { if (sourceUpstreams.size > 0) {
@@ -59,11 +66,6 @@ open class BitcoinMultistream(
super.init() super.init()
} }
open val upstreams: List<BitcoinUpstream>
get() {
return sourceUpstreams
}
open fun getXpubAddresses(): XpubAddresses? { open fun getXpubAddresses(): XpubAddresses? {
return xpubAddresses return xpubAddresses
} }
@@ -103,7 +105,7 @@ open class BitcoinMultistream(
.switchIfEmpty(Mono.error(Exception("No API available for $chain"))) .switchIfEmpty(Mono.error(Exception("No API available for $chain")))
} }
override fun getLocalReader(localEnabled: Boolean): Mono<JsonRpcReader> { override fun getLocalReader(): Mono<JsonRpcReader> {
return Mono.just(callRouter) return Mono.just(callRouter)
} }
@@ -146,7 +148,7 @@ open class BitcoinMultistream(
} }
override fun getEgressSubscription(): EgressSubscription { override fun getEgressSubscription(): EgressSubscription {
return EmptyEgressSubscription() return EmptyEgressSubscription
} }
override fun isRunning(): Boolean { override fun isRunning(): Boolean {

View File

@@ -44,7 +44,7 @@ open class BitcoinReader(
private val unspentReader: UnspentReader = if (esploraClient != null) { private val unspentReader: UnspentReader = if (esploraClient != null) {
EsploraUnspentReader(esploraClient, head) EsploraUnspentReader(esploraClient, head)
} else if (upstreams.upstreams.any { it.isGrpc() && it.getCapabilities().contains(Capability.BALANCE) }) { } else if (upstreams.getUpstreams().any { it.isGrpc() && it.getCapabilities().contains(Capability.BALANCE) }) {
RemoteUnspentReader(upstreams) RemoteUnspentReader(upstreams)
} else { } else {
RpcUnspentReader(upstreams) RpcUnspentReader(upstreams)

View File

@@ -1,75 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.erc20.ERC20Token
import io.emeraldpay.etherjar.hex.Hex32
import io.emeraldpay.etherjar.hex.HexQuantity
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.math.BigInteger
/**
* Query for a ERC20 token balance for an address
*/
open class ERC20Balance {
companion object {
private val log = LoggerFactory.getLogger(ERC20Balance::class.java)
}
open fun getBalance(upstreams: EthereumPosMultiStream, token: ERC20Token, address: Address): Mono<BigInteger> {
return upstreams
// use only up-to-date upstreams
.getApiSource(Selector.HeightMatcher(upstreams.getHead().getCurrentHeight() ?: 0))
.let { getBalance(it, token, address) }
}
open fun getBalance(apis: ApiSource, token: ERC20Token, address: Address): Mono<BigInteger> {
apis.request(1)
return Flux.from(apis)
.flatMap {
getBalance(it.cast(EthereumLikeRpcUpstream::class.java), token, address)
}
.doOnNext {
apis.resolve()
}
.next()
}
open fun getBalance(upstream: EthereumLikeRpcUpstream, token: ERC20Token, address: Address): Mono<BigInteger> {
return upstream
.getIngressReader()
.read(prepareEthCall(token, address, upstream.getHead()))
.flatMap(JsonRpcResponse::requireStringResult)
.map { Hex32.from(it).asQuantity().value }
}
fun prepareEthCall(token: ERC20Token, target: Address, head: Head): JsonRpcRequest {
val call = token
.readBalanceOf(target)
.toJson()
val height = head.getCurrentHeight()?.let { HexQuantity.from(it).toHex() } ?: "latest"
return JsonRpcRequest("eth_call", listOf(call, height))
}
}

View File

@@ -34,7 +34,6 @@ import io.emeraldpay.dshackle.reader.RekeyingReader
import io.emeraldpay.dshackle.reader.SpannedReader import io.emeraldpay.dshackle.reader.SpannedReader
import io.emeraldpay.dshackle.reader.TransformingReader import io.emeraldpay.dshackle.reader.TransformingReader
import io.emeraldpay.dshackle.upstream.CachingReader import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader.Result import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader.Result
@@ -58,7 +57,7 @@ open class EthereumCachingReader(
private val caches: Caches, private val caches: Caches,
callMethodsFactory: Factory<CallMethods>, callMethodsFactory: Factory<CallMethods>,
private val tracer: Tracer, private val tracer: Tracer,
) : Lifecycle, CachingReader { ) : CachingReader {
private val objectMapper: ObjectMapper = Global.objectMapper private val objectMapper: ObjectMapper = Global.objectMapper
private val balanceCache = CurrentBlockCache<Address, Wei>() private val balanceCache = CurrentBlockCache<Address, Wei>()

View File

@@ -1,9 +1,31 @@
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.foundation.ChainOptions.Options
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.LabelsDetector
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.AggregatedPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumLabelsDetector
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder
import io.emeraldpay.dshackle.upstream.generic.ChainSpecific import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import org.springframework.cloud.sleuth.Tracer
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
object EthereumChainSpecific : ChainSpecific { object EthereumChainSpecific : ChainSpecific {
override fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer { override fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer {
@@ -11,4 +33,57 @@ object EthereumChainSpecific : ChainSpecific {
} }
override fun latestBlockRequest() = JsonRpcRequest("eth_getBlockByNumber", listOf("latest", false)) override fun latestBlockRequest() = JsonRpcRequest("eth_getBlockByNumber", listOf("latest", false))
override fun localReaderBuilder(
cachingReader: CachingReader,
methods: CallMethods,
head: Head,
): Mono<JsonRpcReader> {
return Mono.just(EthereumLocalReader(cachingReader as EthereumCachingReader, methods, head))
}
override fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription {
return { ms ->
val pendingTxes: PendingTxesSource = (ms.getAll())
.filter { it is GenericUpstream }
.map { it as GenericUpstream }
.mapNotNull {
(it.getIngressSubscription() as EthereumIngressSubscription).getPendingTxes()
}.let {
if (it.isEmpty()) {
NoPendingTxes()
} else if (it.size == 1) {
it.first()
} else {
AggregatedPendingTxes(it)
}
}
EthereumEgressSubscription(ms, headScheduler, pendingTxes)
}
}
override fun makeCachingReaderBuilder(tracer: Tracer): CachingReaderBuilder {
return { ms, caches, methodsFactory -> EthereumCachingReader(ms, caches, methodsFactory, tracer) }
}
override fun validator(
chain: Chain,
upstream: Upstream,
options: Options,
config: ChainConfig,
): UpstreamValidator? {
return EthereumUpstreamValidator(chain, upstream, options, config)
}
override fun labelDetector(chain: Chain, reader: JsonRpcReader): LabelsDetector? {
return EthereumLabelsDetector(reader, chain)
}
override fun subscriptionTopics(upstream: GenericUpstream): List<String> {
val subs = if (upstream.getCapabilities().contains(Capability.WS_HEAD)) {
listOf(EthereumEgressSubscription.METHOD_NEW_HEADS, EthereumEgressSubscription.METHOD_LOGS)
} else {
listOf()
}
return upstream.getIngressSubscription().getAvailableTopics().plus(subs).toSet().toList()
}
} }

View File

@@ -1,87 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.AbstractChainFees
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.util.function.Tuples
import java.util.function.Function
abstract class EthereumFees(
upstreams: Multistream,
private val reader: EthereumCachingReader,
heightLimit: Int,
) : AbstractChainFees<EthereumFees.EthereumFee, BlockJson<TransactionRefJson>, TransactionRefJson, TransactionJsonSnapshot>(heightLimit, upstreams, extractTx), ChainFees {
companion object {
private val log = LoggerFactory.getLogger(EthereumFees::class.java)
private val extractTx = { block: BlockJson<TransactionRefJson> ->
block.transactions
}
}
abstract fun extractFee(block: BlockJson<TransactionRefJson>, tx: TransactionJsonSnapshot): EthereumFee
override fun readFeesAt(height: Long, selector: TxAt<BlockJson<TransactionRefJson>, TransactionRefJson>): Mono<EthereumFee> {
return reader.blocksByHeightParsed().read(height)
.flatMap { block ->
Mono.justOrEmpty(selector.get(block))
.cast(TransactionRefJson::class.java)
.flatMap { reader.txByHash().read(it.hash) }
.map { tx -> extractFee(block, tx) }
}
}
override fun feeAggregation(mode: ChainFees.Mode): Function<Flux<EthereumFee>, Mono<EthereumFee>> {
if (mode == ChainFees.Mode.MIN_ALWAYS) {
return Function { src ->
src.reduce { a, b ->
EthereumFee(
a.max.coerceAtLeast(b.max),
a.priority.coerceAtLeast(b.priority),
a.paid.coerceAtLeast(b.paid),
Wei.ZERO,
)
}
}
}
return Function { src ->
src.map { Tuples.of(1, it) }
.reduce { a, b ->
Tuples.of(a.t1 + b.t1, a.t2.plus(b.t2))
}.map {
EthereumFee(it.t2.max / it.t1, it.t2.priority / it.t1, it.t2.paid / it.t1, it.t2.base / it.t1)
}
}
}
// ---
data class EthereumFee(val max: Wei, val priority: Wei, val paid: Wei, val base: Wei) {
fun plus(o: EthereumFee): EthereumFee {
return EthereumFee(max + o.max, priority + o.priority, paid + o.paid, base + o.base)
}
}
}

View File

@@ -1,49 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import java.util.function.Function
class EthereumLegacyFees(upstreams: EthereumMultistream, reader: EthereumCachingReader, heightLimit: Int) :
EthereumFees(upstreams, reader, heightLimit) {
companion object {
private val log = LoggerFactory.getLogger(EthereumLegacyFees::class.java)
}
private val toGrpc: Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> = Function {
BlockchainOuterClass.EstimateFeeResponse.newBuilder()
.setEthereumStd(
BlockchainOuterClass.EthereumStdFees.newBuilder()
.setFee(it.paid.amount.toString()),
)
.build()
}
override fun extractFee(block: BlockJson<TransactionRefJson>, tx: TransactionJsonSnapshot): EthereumFee {
return EthereumFee(tx.gasPrice, tx.gasPrice, tx.gasPrice, Wei.ZERO)
}
override fun getResponseBuilder(): Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> {
return toGrpc
}
}

View File

@@ -1,206 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* 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.upstream.ethereum
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_VALID
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumLabelsDetector
import io.emeraldpay.dshackle.upstream.generic.connectors.ConnectorFactory
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.Lifecycle
import reactor.core.Disposable
import reactor.core.publisher.Flux
import java.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
open class EthereumLikeRpcUpstream(
id: String,
hash: Byte,
val chain: Chain,
options: ChainOptions.Options,
role: UpstreamsConfig.UpstreamRole,
targets: CallMethods?,
private val node: QuorumForLabels.QuorumItem?,
connectorFactory: ConnectorFactory,
chainConfig: ChainsConfig.ChainConfig,
skipEnhance: Boolean,
private val eventPublisher: ApplicationEventPublisher?,
) : EthereumLikeUpstream(id, hash, options, role, targets, node, chainConfig), Lifecycle, Upstream, CachesEnabled {
private val validator: EthereumUpstreamValidator = EthereumUpstreamValidator(chain, this, getOptions(), chainConfig.callLimitContract)
protected val connector: GenericConnector = connectorFactory.create(this, chain, skipEnhance)
private val labelsDetector = EthereumLabelsDetector(this.getIngressReader(), chain)
private var hasLiveSubscriptionHead: AtomicBoolean = AtomicBoolean(false)
private var validatorSubscription: Disposable? = null
private var livenessSubscription: Disposable? = null
private var validationSettingsSubscription: Disposable? = null
override fun getCapabilities(): Set<Capability> {
return if (hasLiveSubscriptionHead.get()) {
setOf(Capability.RPC, Capability.BALANCE, Capability.WS_HEAD)
} else {
setOf(Capability.RPC, Capability.BALANCE)
}
}
override fun setCaches(caches: Caches) {
if (connector is CachesEnabled) {
connector.setCaches(caches)
}
}
override fun start() {
log.info("Configured for ${chain.chainName}")
connector.start()
val validSettingsResult = validator.validateUpstreamSettingsOnStartup()
when (validSettingsResult) {
UPSTREAM_FATAL_SETTINGS_ERROR -> {
connector.stop()
log.warn("Upstream ${getId()} couldn't start, invalid upstream settings")
return
}
UPSTREAM_SETTINGS_ERROR -> {
validateUpstreamSettings()
}
else -> {
upstreamStart()
labelsDetector.detectLabels()
.toStream()
.forEach { updateLabels(it) }
}
}
}
private fun validateUpstreamSettings() {
validationSettingsSubscription = Flux.interval(
Duration.ofSeconds(10),
Duration.ofSeconds(20),
).flatMap {
validator.validateUpstreamSettings()
}.subscribe {
when (it) {
UPSTREAM_FATAL_SETTINGS_ERROR -> {
connector.stop()
log.warn("Upstream ${getId()} couldn't start, invalid upstream settings")
disposeValidationSettingsSubscription()
}
UPSTREAM_VALID -> {
upstreamStart()
labelsDetector.detectLabels()
.subscribe { label -> updateLabels(label) }
eventPublisher?.publishEvent(UpstreamChangeEvent(chain, this, UpstreamChangeEvent.ChangeType.ADDED))
disposeValidationSettingsSubscription()
}
else -> {
log.warn("Continue validation of upstream ${getId()}")
}
}
}
}
private fun upstreamStart() {
if (getOptions().disableValidation) {
log.warn("Disable validation for upstream ${this.getId()}")
this.setLag(0)
this.setStatus(UpstreamAvailability.OK)
} else {
log.debug("Start validation for upstream ${this.getId()}")
validatorSubscription = validator.start()
.subscribe(this::setStatus)
}
livenessSubscription = connector.hasLiveSubscriptionHead().subscribe({
hasLiveSubscriptionHead.set(it)
eventPublisher?.publishEvent(UpstreamChangeEvent(chain, this, UpstreamChangeEvent.ChangeType.UPDATED))
}, {
log.debug("Error while checking live subscription for ${getId()}", it)
},)
}
private fun updateLabels(label: Pair<String, String>) {
log.info("Detected label ${label.first} with value ${label.second} for upstream ${getId()}")
node?.labels?.let { labels ->
labels[label.first] = label.second
}
}
override fun getIngressSubscription(): EthereumIngressSubscription {
return connector.getIngressSubscription()
}
override fun getSubscriptionTopics(): List<String> {
val subs = if (getCapabilities().contains(Capability.WS_HEAD)) {
listOf(EthereumEgressSubscription.METHOD_NEW_HEADS, EthereumEgressSubscription.METHOD_LOGS)
} else {
listOf()
}
return getIngressSubscription().getAvailableTopics().plus(subs).toSet().toList()
}
override fun getHead(): Head {
return connector.getHead()
}
override fun stop() {
validatorSubscription?.dispose()
validatorSubscription = null
livenessSubscription?.dispose()
livenessSubscription = null
disposeValidationSettingsSubscription()
connector.stop()
}
override fun isRunning(): Boolean {
return connector.isRunning() && validationSettingsSubscription == null
}
override fun getIngressReader(): JsonRpcReader {
return connector.getIngressReader()
}
override fun isGrpc(): Boolean {
return false
}
@Suppress("UNCHECKED_CAST")
override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
return this as T
}
private fun disposeValidationSettingsSubscription() {
validationSettingsSubscription?.dispose()
validationSettingsSubscription = null
}
}

View File

@@ -1,48 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* 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.upstream.ethereum
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.calls.CallMethods
abstract class EthereumLikeUpstream(
id: String,
hash: Byte,
options: ChainOptions.Options,
role: UpstreamsConfig.UpstreamRole,
targets: CallMethods?,
private val node: QuorumForLabels.QuorumItem?,
val chainConfig: ChainsConfig.ChainConfig,
) : DefaultUpstream(id, hash, options, role, targets, node, chainConfig) {
private val capabilities = setOf(Capability.RPC, Capability.BALANCE)
override fun getCapabilities(): Set<Capability> {
return capabilities
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return node?.let { listOf(it.labels) } ?: emptyList()
}
abstract fun getIngressSubscription(): EthereumIngressSubscription
}

View File

@@ -41,7 +41,6 @@ class EthereumLocalReader(
private val reader: EthereumCachingReader, private val reader: EthereumCachingReader,
private val methods: CallMethods, private val methods: CallMethods,
private val head: Head, private val head: Head,
private val localEnabled: Boolean,
) : JsonRpcReader { ) : JsonRpcReader {
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> { override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
@@ -49,9 +48,6 @@ class EthereumLocalReader(
return Mono.just(methods.executeHardcoded(key.method)) return Mono.just(methods.executeHardcoded(key.method))
.map { JsonRpcResponse(it, null) } .map { JsonRpcResponse(it, null) }
} }
if (!localEnabled) {
return Mono.empty()
}
if (!methods.isCallable(key.method)) { if (!methods.isCallable(key.method)) {
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method")) return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
} }

View File

@@ -1,215 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* 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.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.EmptyHead
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.HeadLagObserver
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.AggregatedPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
import io.emeraldpay.etherjar.domain.BlockHash
import org.springframework.cloud.sleuth.Tracer
import org.springframework.util.ConcurrentReferenceHashMap
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
@Suppress("UNCHECKED_CAST")
open class EthereumMultistream(
chain: Chain,
val upstreams: MutableList<EthereumLikeUpstream>,
caches: Caches,
private val headScheduler: Scheduler,
tracer: Tracer,
) : Multistream(chain, upstreams as MutableList<Upstream>, caches) {
private var head: DynamicMergedHead = DynamicMergedHead(
PriorityForkChoice(),
"ETH Multistream of ${chain.chainCode}",
headScheduler,
)
private val filteredHeads: MutableMap<String, Head> =
ConcurrentReferenceHashMap(16, ConcurrentReferenceHashMap.ReferenceType.WEAK)
private val reader: EthereumCachingReader = EthereumCachingReader(this, this.caches, getMethodsFactory(), tracer)
private var subscribe = EthereumEgressSubscription(this, headScheduler, NoPendingTxes())
init {
this.init()
}
override fun init() {
if (upstreams.size > 0) {
upstreams.forEach { addHead(it) }
}
super.init()
}
override fun onUpstreamsUpdated() {
super.onUpstreamsUpdated()
val pendingTxes: PendingTxesSource = upstreams
.mapNotNull {
it.getIngressSubscription().getPendingTxes()
}.let {
if (it.isEmpty()) {
NoPendingTxes()
} else if (it.size == 1) {
it.first()
} else {
AggregatedPendingTxes(it)
}
}
subscribe = EthereumEgressSubscription(this, headScheduler, pendingTxes)
}
override fun start() {
super.start()
head.start()
onHeadUpdated(head)
reader.start()
}
override fun stop() {
super.stop()
reader.stop()
filteredHeads.clear()
}
override fun addHead(upstream: Upstream) {
val newHead = upstream.getHead()
if (newHead is Lifecycle && !newHead.isRunning()) {
newHead.start()
}
head.addHead(upstream)
}
override fun removeHead(upstreamId: String) {
head.removeHead(upstreamId)
}
override fun makeLagObserver(): HeadLagObserver =
HeadLagObserver(head, upstreams, DistanceExtractor::extractPowDistance, headScheduler, 6).apply {
start()
}
override fun isRunning(): Boolean {
return super.isRunning() || reader.isRunning()
}
override fun getCachingReader(): EthereumCachingReader {
return reader
}
override fun getHead(): Head {
return head
}
override fun tryProxySubscribe(
matcher: Selector.Matcher,
request: BlockchainOuterClass.NativeSubscribeRequest,
): Flux<out Any>? =
upstreams.filter {
matcher.matches(it)
}.takeIf { ups ->
ups.size == 1 && ups.all { it.isGrpc() }
}?.map {
it as GrpcUpstream
}?.map {
it.getBlockchainApi().nativeSubscribe(request)
}?.let {
Flux.merge(it)
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreams.flatMap { it.getLabels() }
}
@Suppress("UNCHECKED_CAST")
override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
return this as T
}
override fun getEgressSubscription(): EgressSubscription {
return subscribe
}
override fun getLocalReader(localEnabled: Boolean): Mono<JsonRpcReader> {
return Mono.just(EthereumLocalReader(reader, getMethods(), getHead(), localEnabled))
}
override fun getHead(mather: Selector.Matcher): Head =
filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
upstreams.filter { mather.matches(it) }
.apply {
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
}.let {
val selected = it.map { it.getHead() }
when (it.size) {
0 -> EmptyHead()
1 -> selected.first()
else -> MergedHead(selected, MostWorkForkChoice(), headScheduler, "Eth head ${it.map { it.getId() }}").apply {
start()
}
}
}
}
override fun getEnrichedHead(mather: Selector.Matcher): Head =
filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
upstreams.filter { mather.matches(it) }
.apply {
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
}.let {
val selected = it.map { source -> source.getHead() }
EnrichedMergedHead(
selected,
getHead(),
headScheduler,
object :
Reader<BlockHash, BlockContainer> {
override fun read(key: BlockHash): Mono<BlockContainer> {
return reader.blocksByHashAsCont().read(key).map { res -> res.data }
}
},
)
}
}
}

View File

@@ -1,59 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import java.util.function.Function
class EthereumPriorityFees(upstreams: Multistream, reader: EthereumCachingReader, heightLimit: Int) :
EthereumFees(upstreams, reader, heightLimit) {
companion object {
private val log = LoggerFactory.getLogger(EthereumPriorityFees::class.java)
}
private val toGrpc: Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> =
Function {
BlockchainOuterClass.EstimateFeeResponse.newBuilder()
.setEthereumExtended(
BlockchainOuterClass.EthereumExtFees.newBuilder()
.setMax(it.max.amount.toString())
.setPriority(it.priority.amount.toString())
.setExpect(it.paid.amount.toString()),
)
.build()
}
override fun extractFee(block: BlockJson<TransactionRefJson>, tx: TransactionJsonSnapshot): EthereumFee {
val baseFee = block.baseFeePerGas ?: Wei.ZERO
if (tx.type == 2) {
// an EIP-1559 Transaction provides Max and Priority fee
val paid = (baseFee + tx.maxPriorityFeePerGas).coerceAtMost(tx.maxFeePerGas)
return EthereumFee(tx.maxFeePerGas, tx.maxPriorityFeePerGas, paid, baseFee)
}
return EthereumFee(tx.gasPrice, (tx.gasPrice - baseFee).coerceAtLeast(Wei.ZERO), tx.gasPrice, baseFee)
}
override fun getResponseBuilder(): Function<EthereumFee, BlockchainOuterClass.EstimateFeeResponse> {
return toGrpc
}
}

View File

@@ -20,12 +20,12 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_VALID
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.domain.Address import io.emeraldpay.etherjar.domain.Address
@@ -47,8 +47,8 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
private val chain: Chain, private val chain: Chain,
private val upstream: Upstream, private val upstream: Upstream,
private val options: ChainOptions.Options, private val options: ChainOptions.Options,
private val callLimitContract: String? = null, private val config: ChainConfig,
) { ) : UpstreamValidator {
companion object { companion object {
private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java) private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java)
val scheduler = val scheduler =
@@ -127,7 +127,7 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
.onErrorReturn(UpstreamAvailability.UNAVAILABLE) .onErrorReturn(UpstreamAvailability.UNAVAILABLE)
} }
fun start(): Flux<UpstreamAvailability> { override fun start(): Flux<UpstreamAvailability> {
return Flux.interval( return Flux.interval(
Duration.ZERO, Duration.ZERO,
Duration.ofSeconds(options.validationInterval.toLong()), Duration.ofSeconds(options.validationInterval.toLong()),
@@ -140,13 +140,9 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
} }
} }
fun validateUpstreamSettingsOnStartup(): ValidateUpstreamSettingsResult { override fun validateUpstreamSettings(): Mono<ValidateUpstreamSettingsResult> {
return validateUpstreamSettings().block() ?: UPSTREAM_FATAL_SETTINGS_ERROR
}
fun validateUpstreamSettings(): Mono<ValidateUpstreamSettingsResult> {
if (options.disableUpstreamValidation) { if (options.disableUpstreamValidation) {
return Mono.just(UPSTREAM_VALID) return Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID)
} }
return Mono.zip( return Mono.zip(
validateChain(), validateChain(),
@@ -159,7 +155,7 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
private fun validateChain(): Mono<ValidateUpstreamSettingsResult> { private fun validateChain(): Mono<ValidateUpstreamSettingsResult> {
if (!options.validateChain) { if (!options.validateChain) {
return Mono.just(UPSTREAM_VALID) return Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID)
} }
return Mono.zip( return Mono.zip(
chainId(), chainId(),
@@ -178,20 +174,20 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
} }
if (isChainValid) { if (isChainValid) {
UPSTREAM_VALID ValidateUpstreamSettingsResult.UPSTREAM_VALID
} else { } else {
UPSTREAM_FATAL_SETTINGS_ERROR ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR
} }
} }
.onErrorResume { .onErrorResume {
log.error("Error during chain validation", it) log.error("Error during chain validation", it)
Mono.just(UPSTREAM_SETTINGS_ERROR) Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR)
} }
} }
private fun validateCallLimit(): Mono<ValidateUpstreamSettingsResult> { private fun validateCallLimit(): Mono<ValidateUpstreamSettingsResult> {
if (!options.validateCallLimit || callLimitContract == null) { if (!options.validateCallLimit || config.callLimitContract == null) {
return Mono.just(UPSTREAM_VALID) return Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID)
} }
return upstream.getIngressReader() return upstream.getIngressReader()
.read( .read(
@@ -199,7 +195,7 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
"eth_call", "eth_call",
listOf( listOf(
TransactionCallJson( TransactionCallJson(
Address.from(callLimitContract), Address.from(config.callLimitContract),
// calling contract with param 200_000, meaning it will generate 200k symbols or response // calling contract with param 200_000, meaning it will generate 200k symbols or response
// f4240 + metadata — ~1 million // f4240 + metadata — ~1 million
HexData.from("0xd8a26e3a00000000000000000000000000000000000000000000000000000000000f4240"), HexData.from("0xd8a26e3a00000000000000000000000000000000000000000000000000000000000f4240"),
@@ -209,7 +205,7 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
), ),
) )
.flatMap(JsonRpcResponse::requireResult) .flatMap(JsonRpcResponse::requireResult)
.map { UPSTREAM_VALID } .map { ValidateUpstreamSettingsResult.UPSTREAM_VALID }
.onErrorResume { .onErrorResume {
if (it.message != null && it.message!!.contains("rpc.returndata.limit")) { if (it.message != null && it.message!!.contains("rpc.returndata.limit")) {
log.warn( log.warn(
@@ -217,7 +213,7 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
"You need to set up your return limit to at least 1_100_000. " + "You need to set up your return limit to at least 1_100_000. " +
"Erigon config example: https://github.com/ledgerwatch/erigon/blob/d014da4dc039ea97caf04ed29feb2af92b7b129d/cmd/utils/flags.go#L369", "Erigon config example: https://github.com/ledgerwatch/erigon/blob/d014da4dc039ea97caf04ed29feb2af92b7b129d/cmd/utils/flags.go#L369",
) )
Mono.just(UPSTREAM_FATAL_SETTINGS_ERROR) Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR)
} else { } else {
Mono.error(it) Mono.error(it)
} }
@@ -233,7 +229,7 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
"message ${ctx.exception().message}", "message ${ctx.exception().message}",
) )
} }
.onErrorReturn(UPSTREAM_SETTINGS_ERROR) .onErrorReturn(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR)
} }
private fun validateOldBlocks(): Mono<ValidateUpstreamSettingsResult> { private fun validateOldBlocks(): Mono<ValidateUpstreamSettingsResult> {
@@ -257,11 +253,11 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
"Node ${upstream.getId()} probably is synced incorrectly, it is not possible to get old blocks", "Node ${upstream.getId()} probably is synced incorrectly, it is not possible to get old blocks",
) )
} }
UPSTREAM_VALID ValidateUpstreamSettingsResult.UPSTREAM_VALID
} }
.onErrorResume { .onErrorResume {
log.warn("Error during old blocks validation", it) log.warn("Error during old blocks validation", it)
Mono.just(UPSTREAM_VALID) Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID)
} }
} }
@@ -290,10 +286,4 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
.doOnError { log.error("Error during execution 'net_version' - ${it.message} for ${upstream.getId()}") } .doOnError { log.error("Error during execution 'net_version' - ${it.message} for ${upstream.getId()}") }
.flatMap(JsonRpcResponse::requireStringResult) .flatMap(JsonRpcResponse::requireStringResult)
} }
enum class ValidateUpstreamSettingsResult {
UPSTREAM_VALID,
UPSTREAM_SETTINGS_ERROR,
UPSTREAM_FATAL_SETTINGS_ERROR,
}
} }

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global.Companion.objectMapper import io.emeraldpay.dshackle.Global.Companion.objectMapper
import io.emeraldpay.dshackle.reader.JsonRpcReader import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.LabelsDetector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumArchiveBlockNumberReader import io.emeraldpay.dshackle.upstream.ethereum.EthereumArchiveBlockNumberReader
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
@@ -14,10 +15,10 @@ import reactor.core.publisher.Mono
class EthereumLabelsDetector( class EthereumLabelsDetector(
private val reader: JsonRpcReader, private val reader: JsonRpcReader,
private val chain: Chain, private val chain: Chain,
) { ) : LabelsDetector {
private val blockNumberReader = EthereumArchiveBlockNumberReader(reader) private val blockNumberReader = EthereumArchiveBlockNumberReader(reader)
fun detectLabels(): Flux<Pair<String, String>> { override fun detectLabels(): Flux<Pair<String, String>> {
return Flux.merge( return Flux.merge(
detectNodeType(), detectNodeType(),
detectArchiveNode(), detectArchiveNode(),

View File

@@ -1,222 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* 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.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
import io.emeraldpay.dshackle.upstream.EmptyHead
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.HeadLagObserver
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.AggregatedPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
import io.emeraldpay.etherjar.domain.BlockHash
import org.springframework.cloud.sleuth.Tracer
import org.springframework.util.ConcurrentReferenceHashMap
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
@Suppress("UNCHECKED_CAST")
open class EthereumPosMultiStream(
chain: Chain,
val upstreams: MutableList<EthereumLikeUpstream>,
caches: Caches,
private val headScheduler: Scheduler,
tracer: Tracer,
) : Multistream(chain, upstreams as MutableList<Upstream>, caches) {
private var head: DynamicMergedHead = DynamicMergedHead(
PriorityForkChoice(),
"ETH Pos Multistream of ${chain.chainCode}",
headScheduler,
)
private val reader: EthereumCachingReader = EthereumCachingReader(this, this.caches, getMethodsFactory(), tracer)
private var subscribe = EthereumEgressSubscription(this, headScheduler, NoPendingTxes())
private val filteredHeads: MutableMap<String, Head> =
ConcurrentReferenceHashMap(16, ConcurrentReferenceHashMap.ReferenceType.WEAK)
init {
this.init()
}
override fun init() {
if (upstreams.size > 0) {
upstreams.forEach { addHead(it) }
}
super.init()
}
override fun start() {
super.start()
head.start()
onHeadUpdated(head)
reader.start()
}
override fun stop() {
super.stop()
reader.stop()
filteredHeads.clear()
}
override fun addHead(upstream: Upstream) {
val newHead = upstream.getHead()
if (newHead is Lifecycle && !newHead.isRunning()) {
newHead.start()
}
head.addHead(upstream)
}
override fun removeHead(upstreamId: String) {
head.removeHead(upstreamId)
}
override fun isRunning(): Boolean {
return super.isRunning() || reader.isRunning()
}
override fun makeLagObserver(): HeadLagObserver =
HeadLagObserver(head, upstreams, DistanceExtractor::extractPriorityDistance, headScheduler, 6).apply {
start()
}
override fun getCachingReader(): EthereumCachingReader {
return reader
}
override fun getHead(): Head {
return head
}
override fun tryProxySubscribe(
matcher: Selector.Matcher,
request: BlockchainOuterClass.NativeSubscribeRequest,
): Flux<out Any>? =
upstreams.filter {
matcher.matches(it)
}.takeIf { ups ->
ups.size == 1 && ups.all { it.isGrpc() }
}?.map {
it as GrpcUpstream
}?.map {
it.proxySubscribe(request)
}?.let {
Flux.merge(it)
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreams.flatMap { it.getLabels() }
}
@Suppress("UNCHECKED_CAST")
override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
return this as T
}
override fun getLocalReader(localEnabled: Boolean): Mono<JsonRpcReader> {
return Mono.just(EthereumLocalReader(reader, getMethods(), getHead(), localEnabled))
}
override fun getEgressSubscription(): EthereumEgressSubscription {
return subscribe
}
override fun getHead(mather: Selector.Matcher): Head =
if (mather == Selector.empty || mather == Selector.anyLabel) {
head
} else {
filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
upstreams.filter { mather.matches(it) }
.apply {
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
}
.let {
val selected = it.map { it.getHead() }
when (it.size) {
0 -> EmptyHead()
1 -> selected.first()
else -> MergedHead(
selected,
PriorityForkChoice(),
headScheduler,
"ETH head for ${it.map { it.getId() }}",
).apply {
start()
}
}
}
}
}
override fun getEnrichedHead(mather: Selector.Matcher): Head =
filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
upstreams.filter { mather.matches(it) }
.apply {
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
}.let {
val selected = it.map { source -> source.getHead() }
EnrichedMergedHead(
selected,
getHead(),
headScheduler,
object :
Reader<BlockHash, BlockContainer> {
override fun read(key: BlockHash): Mono<BlockContainer> {
return reader.blocksByHashAsCont().read(key).map { res -> res.data }
}
},
)
}
}
override fun onUpstreamsUpdated() {
super.onUpstreamsUpdated()
val pendingTxes: PendingTxesSource = upstreams
.mapNotNull {
it.getIngressSubscription().getPendingTxes()
}.let {
if (it.isEmpty()) {
NoPendingTxes()
} else if (it.size == 1) {
it.first()
} else {
AggregatedPendingTxes(it)
}
}
subscribe = EthereumEgressSubscription(this, headScheduler, pendingTxes)
}
}

View File

@@ -3,19 +3,53 @@ package io.emeraldpay.dshackle.upstream.generic
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.STARKNET import io.emeraldpay.dshackle.BlockchainType.STARKNET
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.LabelsDetector
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific import io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific
import org.apache.commons.collections4.Factory
import org.springframework.cloud.sleuth.Tracer
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
typealias SubscriptionBuilder = (Multistream) -> EgressSubscription
typealias LocalReaderBuilder = (CachingReader, CallMethods, Head) -> Mono<JsonRpcReader>
typealias CachingReaderBuilder = (Multistream, Caches, Factory<CallMethods>) -> CachingReader
interface ChainSpecific { interface ChainSpecific {
fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer
fun latestBlockRequest(): JsonRpcRequest fun latestBlockRequest(): JsonRpcRequest
fun localReaderBuilder(cachingReader: CachingReader, methods: CallMethods, head: Head): Mono<JsonRpcReader>
fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription
fun makeCachingReaderBuilder(tracer: Tracer): CachingReaderBuilder
fun validator(chain: Chain, upstream: Upstream, options: ChainOptions.Options, config: ChainConfig): UpstreamValidator?
fun labelDetector(chain: Chain, reader: JsonRpcReader): LabelsDetector?
fun subscriptionTopics(upstream: GenericUpstream): List<String>
} }
object ChainSpecificRegistry { object ChainSpecificRegistry {
@JvmStatic
fun resolve(chain: Chain): ChainSpecific { fun resolve(chain: Chain): ChainSpecific {
if (BlockchainType.from(chain) == STARKNET) { if (BlockchainType.from(chain) == STARKNET) {
return StarknetChainSpecific return StarknetChainSpecific

View File

@@ -16,6 +16,7 @@
*/ */
package io.emeraldpay.dshackle.upstream.generic package io.emeraldpay.dshackle.upstream.generic
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
@@ -24,24 +25,42 @@ import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.DistanceExtractor import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.DynamicMergedHead import io.emeraldpay.dshackle.upstream.DynamicMergedHead
import io.emeraldpay.dshackle.upstream.EgressSubscription import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.EmptyEgressSubscription import io.emeraldpay.dshackle.upstream.EmptyHead
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.HeadLagObserver import io.emeraldpay.dshackle.upstream.HeadLagObserver
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Selector.Matcher import io.emeraldpay.dshackle.upstream.Selector.Matcher
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
import org.springframework.util.ConcurrentReferenceHashMap
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.WEAK
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler import reactor.core.scheduler.Scheduler
@Suppress("UNCHECKED_CAST")
open class GenericMultistream( open class GenericMultistream(
chain: Chain, chain: Chain,
val upstreams: MutableList<GenericUpstream>, private val upstreams: MutableList<Upstream>,
caches: Caches, caches: Caches,
private val headScheduler: Scheduler, private val headScheduler: Scheduler,
) : Multistream(chain, upstreams as MutableList<Upstream>, caches) { cachingReaderBuilder: CachingReaderBuilder,
private val localReaderBuilder: LocalReaderBuilder,
private val subscriptionBuilder: SubscriptionBuilder,
) : Multistream(chain, caches) {
private val cachingReader = cachingReaderBuilder(this, caches, getMethodsFactory())
override fun getUpstreams(): MutableList<out Upstream> {
return upstreams
}
override fun addUpstreamInternal(u: Upstream) {
upstreams.add(u as GenericUpstream)
}
private var head: DynamicMergedHead = DynamicMergedHead( private var head: DynamicMergedHead = DynamicMergedHead(
PriorityForkChoice(), PriorityForkChoice(),
@@ -53,6 +72,8 @@ open class GenericMultistream(
this.init() this.init()
} }
private var subscription: EgressSubscription = subscriptionBuilder(this)
override fun init() { override fun init() {
if (upstreams.size > 0) { if (upstreams.size > 0) {
upstreams.forEach { addHead(it) } upstreams.forEach { addHead(it) }
@@ -60,10 +81,20 @@ open class GenericMultistream(
super.init() super.init()
} }
private val filteredHeads: MutableMap<String, Head> =
ConcurrentReferenceHashMap(16, WEAK)
override fun start() { override fun start() {
super.start() super.start()
head.start() head.start()
onHeadUpdated(head) onHeadUpdated(head)
cachingReader.start()
}
override fun stop() {
super.stop()
cachingReader.stop()
filteredHeads.clear()
} }
override fun addHead(upstream: Upstream) { override fun addHead(upstream: Upstream) {
@@ -78,17 +109,45 @@ open class GenericMultistream(
head.removeHead(upstreamId) head.removeHead(upstreamId)
} }
override fun isRunning(): Boolean {
return super.isRunning() || cachingReader.isRunning()
}
override fun makeLagObserver(): HeadLagObserver = override fun makeLagObserver(): HeadLagObserver =
HeadLagObserver(head, upstreams, DistanceExtractor::extractPriorityDistance, headScheduler, 6).apply { HeadLagObserver(head, upstreams, DistanceExtractor::extractPriorityDistance, headScheduler, 6).apply {
start() start()
} }
override fun getCachingReader(): CachingReader? { override fun getCachingReader(): CachingReader? {
return null return cachingReader
} }
override fun getHead(mather: Matcher): Head { override fun getHead(mather: Matcher): Head {
return getHead() if (mather == Selector.empty || mather == Selector.anyLabel) {
return head
} else {
return filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
upstreams.filter { mather.matches(it) }
.apply {
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
}
.let {
val selected = it.map { it.getHead() }
when (it.size) {
0 -> EmptyHead()
1 -> selected.first()
else -> MergedHead(
selected,
PriorityForkChoice(),
headScheduler,
"Head for ${it.map { it.getId() }}",
).apply {
start()
}
}
}
}
}
} }
override fun getHead(): Head { override fun getHead(): Head {
@@ -111,11 +170,32 @@ open class GenericMultistream(
return this as T return this as T
} }
override fun getLocalReader(localEnabled: Boolean): Mono<JsonRpcReader> { override fun getLocalReader(): Mono<JsonRpcReader> {
return Mono.just(LocalReader(getMethods())) return localReaderBuilder(cachingReader, getMethods(), getHead())
} }
override fun getEgressSubscription(): EgressSubscription { override fun getEgressSubscription(): EgressSubscription {
return EmptyEgressSubscription() return subscription
} }
override fun onUpstreamsUpdated() {
super.onUpstreamsUpdated()
subscription = subscriptionBuilder(this)
}
override fun tryProxySubscribe(
matcher: Matcher,
request: BlockchainOuterClass.NativeSubscribeRequest,
): Flux<out Any>? =
upstreams.filter {
matcher.matches(it)
}.takeIf { ups ->
ups.size == 1 && ups.all { it.isGrpc() }
}?.map {
it as GrpcUpstream
}?.map {
it.proxySubscribe(request)
}?.let {
Flux.merge(it)
}
} }

View File

@@ -8,21 +8,27 @@ import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.reader.JsonRpcReader import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent.ChangeType.UPDATED
import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.LabelsDetectorBuilder
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.UpstreamValidatorBuilder
import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.generic.connectors.ConnectorFactory import io.emeraldpay.dshackle.upstream.generic.connectors.ConnectorFactory
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.Disposable import reactor.core.Disposable
import reactor.core.publisher.Flux
import java.time.Duration
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class GenericUpstream( open class GenericUpstream(
id: String, id: String,
val chain: Chain, val chain: Chain,
hash: Byte, hash: Byte,
@@ -30,14 +36,23 @@ class GenericUpstream(
role: UpstreamsConfig.UpstreamRole, role: UpstreamsConfig.UpstreamRole,
targets: CallMethods?, targets: CallMethods?,
private val node: QuorumForLabels.QuorumItem?, private val node: QuorumForLabels.QuorumItem?,
val chainConfig: ChainsConfig.ChainConfig, chainConfig: ChainsConfig.ChainConfig,
connectorFactory: ConnectorFactory, connectorFactory: ConnectorFactory,
private val eventPublisher: ApplicationEventPublisher?, private val eventPublisher: ApplicationEventPublisher?,
validatorBuilder: UpstreamValidatorBuilder,
labelsDetectorBuilder: LabelsDetectorBuilder,
private val subscriptionTopics: (GenericUpstream) -> List<String>,
) : DefaultUpstream(id, hash, null, UpstreamAvailability.OK, options, role, targets, node, chainConfig), Lifecycle { ) : DefaultUpstream(id, hash, null, UpstreamAvailability.OK, options, role, targets, node, chainConfig), Lifecycle {
private val validator: UpstreamValidator? = validatorBuilder(chain, this, getOptions(), chainConfig)
private var validatorSubscription: Disposable? = null
private var validationSettingsSubscription: Disposable? = null
private val hasLiveSubscriptionHead: AtomicBoolean = AtomicBoolean(false) private val hasLiveSubscriptionHead: AtomicBoolean = AtomicBoolean(false)
private val connector: GenericConnector = connectorFactory.create(this, chain, true) protected val connector: GenericConnector = connectorFactory.create(this, chain, true)
private var livenessSubscription: Disposable? = null private var livenessSubscription: Disposable? = null
private val labelsDetector = labelsDetectorBuilder(chain, this.getIngressReader())
override fun getHead(): Head { override fun getHead(): Head {
return connector.getHead() return connector.getHead()
} }
@@ -51,10 +66,7 @@ class GenericUpstream(
} }
override fun getSubscriptionTopics(): List<String> { override fun getSubscriptionTopics(): List<String> {
// should be implemented in next iterations return subscriptionTopics(this)
// starknet doesn't have any subscriptions at all
// polkadot serves subscriptions like separate json-rpc methods
return emptyList()
} }
// outdated, looks like applicable only for bitcoin and our ws_head trick // outdated, looks like applicable only for bitcoin and our ws_head trick
@@ -83,19 +95,107 @@ class GenericUpstream(
log.info("Configured for ${chain.chainName}") log.info("Configured for ${chain.chainName}")
connector.start() connector.start()
if (validator != null) {
val validSettingsResult = validator.validateUpstreamSettingsOnStartup()
when (validSettingsResult) {
ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR -> {
connector.stop()
log.warn("Upstream ${getId()} couldn't start, invalid upstream settings")
return
}
ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR -> {
validateUpstreamSettings()
}
else -> {
upstreamStart()
}
}
} else {
upstreamStart()
}
}
private fun validateUpstreamSettings() {
if (validator != null) {
validationSettingsSubscription = Flux.interval(
Duration.ofSeconds(10),
Duration.ofSeconds(20),
).flatMap {
validator.validateUpstreamSettings()
}.subscribe {
when (it) {
ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR -> {
connector.stop()
disposeValidationSettingsSubscription()
}
ValidateUpstreamSettingsResult.UPSTREAM_VALID -> {
upstreamStart()
eventPublisher?.publishEvent(
UpstreamChangeEvent(
chain,
this,
UpstreamChangeEvent.ChangeType.ADDED,
),
)
disposeValidationSettingsSubscription()
}
else -> {
log.warn("Continue validation of upstream ${getId()}")
}
}
}
}
}
private fun detectLabels() {
labelsDetector?.detectLabels()?.subscribe { label -> updateLabels(label) }
}
private fun upstreamStart() {
if (getOptions().disableValidation) {
log.warn("Disable validation for upstream ${this.getId()}")
this.setLag(0)
this.setStatus(UpstreamAvailability.OK)
} else {
log.debug("Start validation for upstream ${this.getId()}")
validatorSubscription = validator?.start()
?.subscribe(this::setStatus)
}
livenessSubscription = connector.hasLiveSubscriptionHead().subscribe({ livenessSubscription = connector.hasLiveSubscriptionHead().subscribe({
hasLiveSubscriptionHead.set(it) hasLiveSubscriptionHead.set(it)
eventPublisher?.publishEvent(UpstreamChangeEvent(chain, this, UPDATED)) eventPublisher?.publishEvent(UpstreamChangeEvent(chain, this, UpstreamChangeEvent.ChangeType.UPDATED))
}, { }, {
log.debug("Error while checking live subscription for ${getId()}", it) log.debug("Error while checking live subscription for ${getId()}", it)
},) },)
detectLabels()
} }
override fun stop() { override fun stop() {
validatorSubscription?.dispose()
validatorSubscription = null
livenessSubscription?.dispose() livenessSubscription?.dispose()
livenessSubscription = null livenessSubscription = null
disposeValidationSettingsSubscription()
connector.stop() connector.stop()
} }
private fun disposeValidationSettingsSubscription() {
validationSettingsSubscription?.dispose()
validationSettingsSubscription = null
}
private fun updateLabels(label: Pair<String, String>) {
log.info("Detected label ${label.first} with value ${label.second} for upstream ${getId()}")
node?.labels?.let { labels ->
labels[label.first] = label.second
}
}
fun getIngressSubscription(): IngressSubscription {
return connector.getIngressSubscription()
}
override fun isRunning() = connector.isRunning() override fun isRunning() = connector.isRunning()
} }

View File

@@ -2,8 +2,8 @@ package io.emeraldpay.dshackle.upstream.generic.connectors
import io.emeraldpay.dshackle.reader.JsonRpcReader import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
interface GenericConnector : Lifecycle { interface GenericConnector : Lifecycle {
@@ -13,5 +13,5 @@ interface GenericConnector : Lifecycle {
fun getIngressReader(): JsonRpcReader fun getIngressReader(): JsonRpcReader
fun getIngressSubscription(): EthereumIngressSubscription fun getIngressSubscription(): IngressSubscription
} }

View File

@@ -7,9 +7,9 @@ import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.BlockValidator import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.MergedHead import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead
import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator
import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription
@@ -145,7 +145,7 @@ class GenericRpcConnector(
return directReader return directReader
} }
override fun getIngressSubscription(): EthereumIngressSubscription { override fun getIngressSubscription(): IngressSubscription {
return NoEthereumIngressSubscription.DEFAULT return NoEthereumIngressSubscription.DEFAULT
} }

View File

@@ -4,6 +4,7 @@ import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.BlockValidator import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead
import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator
@@ -74,7 +75,7 @@ class GenericWsConnector(
return reader return reader
} }
override fun getIngressSubscription(): EthereumIngressSubscription { override fun getIngressSubscription(): IngressSubscription {
return subscriptions return subscriptions
} }

View File

@@ -1,223 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* 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.upstream.grpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc.ReactorBlockchainStub
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.BuildInfo
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeUpstream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumDshackleIngressSubscription
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.rpc.RpcException
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import java.math.BigInteger
import java.time.Instant
import java.util.Locale
import java.util.concurrent.TimeoutException
import java.util.function.Function
open class EthereumGrpcUpstream(
private val parentId: String,
hash: Byte,
role: UpstreamsConfig.UpstreamRole,
private val chain: Chain,
private val remote: ReactorBlockchainStub,
client: JsonRpcGrpcClient,
overrideLabels: UpstreamsConfig.Labels?,
chainConfig: ChainsConfig.ChainConfig,
headScheduler: Scheduler,
) : EthereumLikeUpstream(
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
hash,
ChainOptions.PartialOptions.getDefaults().buildOptions(),
role,
null,
null,
chainConfig,
),
GrpcUpstream,
Lifecycle {
private val blockConverter: Function<BlockchainOuterClass.ChainHead, BlockContainer> = Function { value ->
val parentHash =
if (value.parentBlockId.isBlank()) {
null
} else {
BlockId.from(BlockHash.from("0x" + value.parentBlockId))
}
val block = BlockContainer(
value.height,
BlockId.from(BlockHash.from("0x" + value.blockId)),
BigInteger(1, value.weight.toByteArray()),
Instant.ofEpochMilli(value.timestamp),
false,
null,
null,
parentHash,
)
block
}
override fun getSubscriptionTopics(): List<String> {
return subscriptionTopics
}
private val reloadBlock: Function<BlockContainer, Publisher<BlockContainer>> = Function { existingBlock ->
// head comes without transaction data
// need to download transactions for the block
defaultReader.read(JsonRpcRequest("eth_getBlockByHash", listOf(existingBlock.hash.toHexWithPrefix(), false)))
.flatMap(JsonRpcResponse::requireResult)
.map {
BlockContainer.fromEthereumJson(it, getId())
}
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
.doOnError { t ->
setStatus(UpstreamAvailability.UNAVAILABLE)
val msg = "Failed to download block data for chain $chain on $parentId"
if (t is RpcException || t is TimeoutException) {
log.warn("$msg. Message: ${t.message}")
} else {
log.error(msg, t)
}
}
}
private val upstreamStatus = GrpcUpstreamStatus(overrideLabels)
private val grpcHead = GrpcHead(
getId(),
chain,
this,
remote,
blockConverter,
reloadBlock,
MostWorkForkChoice(),
headScheduler,
)
private var capabilities: Set<Capability> = emptySet()
private val buildInfo: BuildInfo = BuildInfo()
private var subscriptionTopics = listOf<String>()
private val defaultReader: JsonRpcReader = client.getReader()
var timeout = Defaults.timeout
private val ethereumSubscriptions = EthereumDshackleIngressSubscription(chain, remote)
override fun getBlockchainApi(): ReactorBlockchainStub {
return remote
}
override fun proxySubscribe(request: BlockchainOuterClass.NativeSubscribeRequest): Flux<out Any> =
remote.nativeSubscribe(request)
override fun start() {
}
override fun isRunning(): Boolean {
return true
}
override fun stop() {
}
override fun getBuildInfo(): BuildInfo {
return buildInfo
}
override fun update(conf: BlockchainOuterClass.DescribeChain, buildInfo: BlockchainOuterClass.BuildInfo): Boolean {
val newBuildInfo = BuildInfo.extract(buildInfo)
val buildInfoChanged = this.buildInfo.update(newBuildInfo)
val newCapabilities = RemoteCapabilities.extract(conf)
val upstreamStatusChanged = (upstreamStatus.update(conf) || (newCapabilities != capabilities)).also {
capabilities = newCapabilities
}
conf.status?.let { status -> onStatus(status) }
val subsChanged = (conf.supportedSubscriptionsList != subscriptionTopics).also {
subscriptionTopics = conf.supportedSubscriptionsList
}
return buildInfoChanged || upstreamStatusChanged || subsChanged
}
override fun getQuorumByLabel(): QuorumForLabels {
return upstreamStatus.getNodes()
}
// ------------------------------------------------------------------------------------------
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreamStatus.getLabels()
}
override fun getIngressSubscription(): EthereumIngressSubscription {
return ethereumSubscriptions
}
override fun getMethods(): CallMethods {
return upstreamStatus.getCallMethods()
}
override fun isAvailable(): Boolean {
return super.isAvailable() && grpcHead.getCurrent() != null && getQuorumByLabel().getAll().any {
it.quorum > 0
}
}
override fun getHead(): Head {
return grpcHead
}
override fun getIngressReader(): JsonRpcReader {
return defaultReader
}
@Suppress("UNCHECKED_CAST")
override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
return this as T
}
override fun getCapabilities(): Set<Capability> {
return capabilities
}
override fun isGrpc(): Boolean {
return true
}
}

View File

@@ -28,13 +28,11 @@ import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.BuildInfo import io.emeraldpay.dshackle.upstream.BuildInfo
import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeUpstream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumDshackleIngressSubscription
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.BlockHash
@@ -45,7 +43,7 @@ import java.time.Instant
import java.util.Locale import java.util.Locale
import java.util.function.Function import java.util.function.Function
open class EthereumPosGrpcUpstream( open class GenericGrpcUpstream(
parentId: String, parentId: String,
hash: Byte, hash: Byte,
role: UpstreamsConfig.UpstreamRole, role: UpstreamsConfig.UpstreamRole,
@@ -56,7 +54,7 @@ open class EthereumPosGrpcUpstream(
overrideLabels: UpstreamsConfig.Labels?, overrideLabels: UpstreamsConfig.Labels?,
chainConfig: ChainsConfig.ChainConfig, chainConfig: ChainsConfig.ChainConfig,
headScheduler: Scheduler, headScheduler: Scheduler,
) : EthereumLikeUpstream( ) : DefaultUpstream(
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}", "${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
hash, hash,
ChainOptions.PartialOptions.getDefaults().buildOptions(), ChainOptions.PartialOptions.getDefaults().buildOptions(),
@@ -103,7 +101,8 @@ open class EthereumPosGrpcUpstream(
private val buildInfo: BuildInfo = BuildInfo() private val buildInfo: BuildInfo = BuildInfo()
private val defaultReader: JsonRpcReader = client.getReader() private val defaultReader: JsonRpcReader = client.getReader()
private val ethereumSubscriptions = EthereumDshackleIngressSubscription(chain, remote)
// private val ethereumSubscriptions = EthereumDshackleIngressSubscription(chain, remote)
private var subscriptionTopics = listOf<String>() private var subscriptionTopics = listOf<String>()
override fun start() { override fun start() {
@@ -155,10 +154,6 @@ open class EthereumPosGrpcUpstream(
return upstreamStatus.getLabels() return upstreamStatus.getLabels()
} }
override fun getIngressSubscription(): EthereumIngressSubscription {
return ethereumSubscriptions
}
override fun getMethods(): CallMethods { override fun getMethods(): CallMethods {
return upstreamStatus.getCallMethods() return upstreamStatus.getCallMethods()
} }

View File

@@ -25,6 +25,7 @@ import io.emeraldpay.api.proto.Common.ChainRef.UNRECOGNIZED
import io.emeraldpay.api.proto.ReactorAuthGrpc import io.emeraldpay.api.proto.ReactorAuthGrpc
import io.emeraldpay.api.proto.ReactorBlockchainGrpc import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.FileResolver
@@ -34,7 +35,6 @@ import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.grpc.auth.AuthException import io.emeraldpay.dshackle.upstream.grpc.auth.AuthException
import io.emeraldpay.dshackle.upstream.grpc.auth.ClientAuthenticationInterceptor import io.emeraldpay.dshackle.upstream.grpc.auth.ClientAuthenticationInterceptor
@@ -246,42 +246,37 @@ class GrpcUpstreams(
return sslContext.build() return sslContext.build()
} }
private val creators: Map<BlockchainType, (chain: Chain, client: JsonRpcGrpcClient) -> DefaultUpstream> = mapOf(
BlockchainType.EVM_POW to { chain, rpcClient ->
EthereumGrpcUpstream(
id,
hash,
role,
chain,
client,
rpcClient,
labels,
chainsConfig.resolve(chain.chainName),
headScheduler,
)
},
BlockchainType.EVM_POS to { chain, rpcClient ->
EthereumPosGrpcUpstream(
id,
hash,
role,
chain,
client,
rpcClient,
nodeRating,
labels,
chainsConfig.resolve(chain.chainName),
headScheduler,
)
},
BlockchainType.BITCOIN to { chain, rpcClient ->
BitcoinGrpcUpstream(id, role, chain, client, rpcClient, labels, chainsConfig.resolve(chain.chainCode), headScheduler)
},
)
private fun getOrCreate(chain: Chain): UpstreamChangeEvent { private fun getOrCreate(chain: Chain): UpstreamChangeEvent {
val metrics = makeMetrics(chain) val metrics = makeMetrics(chain)
val creator = creators.getValue(BlockchainType.from(chain)) val creator = if (BlockchainType.from(chain) != BITCOIN) {
{ ch: Chain, rpcClient: JsonRpcGrpcClient ->
GenericGrpcUpstream(
id,
hash,
role,
ch,
client,
rpcClient,
nodeRating,
labels,
chainsConfig.resolve(chain.chainName),
headScheduler,
)
}
} else {
{ ch: Chain, rpcClient: JsonRpcGrpcClient ->
BitcoinGrpcUpstream(
id,
role,
chain,
client,
rpcClient,
labels,
chainsConfig.resolve(chain.chainCode),
headScheduler,
)
}
}
return getOrCreate(chain, metrics, creator) return getOrCreate(chain, metrics, creator)
} }
@@ -315,7 +310,7 @@ class GrpcUpstreams(
val rpcClient = JsonRpcGrpcClient(client, chain, metrics) val rpcClient = JsonRpcGrpcClient(client, chain, metrics)
val created = creator(chain, rpcClient) val created = creator(chain, rpcClient)
known[chain] = created known[chain] = created
if (created is Lifecycle) created.start() created.start()
UpstreamChangeEvent(chain, created, UpstreamChangeEvent.ChangeType.ADDED) UpstreamChangeEvent(chain, created, UpstreamChangeEvent.ChangeType.ADDED)
} else { } else {
UpstreamChangeEvent(chain, current, UpstreamChangeEvent.ChangeType.REVALIDATED) UpstreamChangeEvent(chain, current, UpstreamChangeEvent.ChangeType.REVALIDATED)

View File

@@ -2,12 +2,32 @@ package io.emeraldpay.dshackle.upstream.starknet
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.foundation.ChainOptions.Options
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.EmptyEgressSubscription
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.LabelsDetector
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.NoopCachingReader
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder
import io.emeraldpay.dshackle.upstream.generic.ChainSpecific import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.generic.LocalReader
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import org.springframework.cloud.sleuth.Tracer
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import java.math.BigInteger import java.math.BigInteger
import java.time.Instant import java.time.Instant
@@ -30,7 +50,41 @@ object StarknetChainSpecific : ChainSpecific {
) )
} }
override fun latestBlockRequest(): JsonRpcRequest = JsonRpcRequest("starknet_getBlockWithTxHashes", listOf("latest")) override fun latestBlockRequest(): JsonRpcRequest =
JsonRpcRequest("starknet_getBlockWithTxHashes", listOf("latest"))
override fun localReaderBuilder(
cachingReader: CachingReader,
methods: CallMethods,
head: Head,
): Mono<JsonRpcReader> {
return Mono.just(LocalReader(methods))
}
override fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription {
return { _ -> EmptyEgressSubscription }
}
override fun makeCachingReaderBuilder(tracer: Tracer): CachingReaderBuilder {
return { _, _, _ -> NoopCachingReader }
}
override fun validator(
chain: Chain,
upstream: Upstream,
options: Options,
config: ChainConfig,
): UpstreamValidator? {
return null
}
override fun labelDetector(chain: Chain, reader: JsonRpcReader): LabelsDetector? {
return null
}
override fun subscriptionTopics(upstream: GenericUpstream): List<String> {
return emptyList()
}
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

View File

@@ -47,14 +47,4 @@ class CacheConfigReaderSpec extends Specification {
//later may be not null if we support something else besides Redis //later may be not null if we support something else besides Redis
act == null act == null
} }
def "Local read disabled"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("cache-local-router-disabled.yaml")
when:
def act = reader.read(config)
then:
!act.requestsCacheEnabled
}
} }

View File

@@ -54,7 +54,7 @@ class NativeCallSpec extends Specification {
ObjectMapper objectMapper = Global.objectMapper ObjectMapper objectMapper = Global.objectMapper
def nativeCall(MultistreamHolder upstreams = null, ResponseSigner signer = null, Boolean enableCache = true, Boolean passthrough = false) { def nativeCall(MultistreamHolder upstreams = null, ResponseSigner signer = null, Boolean passthrough = false) {
if (upstreams == null) { if (upstreams == null) {
upstreams = Stub(MultistreamHolder) upstreams = Stub(MultistreamHolder)
@@ -65,7 +65,6 @@ class NativeCallSpec extends Specification {
def config = new MainConfig() def config = new MainConfig()
def cacheConfig = new CacheConfig() def cacheConfig = new CacheConfig()
cacheConfig.requestsCacheEnabled = enableCache
config.cache = cacheConfig config.cache = cacheConfig
config.passthrough = passthrough config.passthrough = passthrough
@@ -77,7 +76,7 @@ class NativeCallSpec extends Specification {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null)) 1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null))
} }
def upstream = Mock(Multistream) { def upstream = Mock(Multistream) {
1 * getLocalReader(_) >> Mono.just(routedApi) 1 * getLocalReader() >> Mono.just(routedApi)
} }
def nativeCall = nativeCall() def nativeCall = nativeCall()
@@ -98,7 +97,7 @@ class NativeCallSpec extends Specification {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message")) 1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message"))
} }
def upstream = Mock(Multistream) { def upstream = Mock(Multistream) {
1 * getLocalReader(_) >> Mono.just(routedApi) 1 * getLocalReader() >> Mono.just(routedApi)
} }
def nativeCall = nativeCall() def nativeCall = nativeCall()

View File

@@ -21,7 +21,7 @@ import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.test.MultistreamHolderMock import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumEgressSubscription import io.emeraldpay.dshackle.upstream.ethereum.EthereumEgressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream import io.emeraldpay.dshackle.upstream.generic.GenericMultistream
import io.emeraldpay.dshackle.upstream.signature.NoSigner import io.emeraldpay.dshackle.upstream.signature.NoSigner
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.test.StepVerifier import reactor.test.StepVerifier
@@ -42,7 +42,7 @@ class NativeSubscribeSpec extends Specification {
def subscribe = Mock(EthereumEgressSubscription) { def subscribe = Mock(EthereumEgressSubscription) {
1 * it.subscribe("newHeads", null, _ as Selector.AnyLabelMatcher) >> Flux.just("{}") 1 * it.subscribe("newHeads", null, _ as Selector.AnyLabelMatcher) >> Flux.just("{}")
} }
def up = Mock(EthereumPosMultiStream) { def up = Mock(GenericMultistream) {
1 * it.tryProxySubscribe(_ as Selector.AnyLabelMatcher, call) >> null 1 * it.tryProxySubscribe(_ as Selector.AnyLabelMatcher, call) >> null
1 * it.getEgressSubscription() >> subscribe 1 * it.getEgressSubscription() >> subscribe
} }
@@ -81,7 +81,7 @@ class NativeSubscribeSpec extends Specification {
ok ok
}, _ as Selector.AnyLabelMatcher) >> Flux.just("{}") }, _ as Selector.AnyLabelMatcher) >> Flux.just("{}")
} }
def up = Mock(EthereumPosMultiStream) { def up = Mock(GenericMultistream) {
1 * it.tryProxySubscribe(_ as Selector.AnyLabelMatcher, call) >> null 1 * it.tryProxySubscribe(_ as Selector.AnyLabelMatcher, call) >> null
1 * it.getEgressSubscription() >> subscribe 1 * it.getEgressSubscription() >> subscribe
} }
@@ -104,7 +104,7 @@ class NativeSubscribeSpec extends Specification {
.setChainValue(Chain.ETHEREUM__MAINNET.id) .setChainValue(Chain.ETHEREUM__MAINNET.id)
.setMethod("newHeads") .setMethod("newHeads")
.build() .build()
def up = Mock(EthereumPosMultiStream) { def up = Mock(GenericMultistream) {
1 * it.tryProxySubscribe(_ as Selector.AnyLabelMatcher, call) >> Flux.just("{}") 1 * it.tryProxySubscribe(_ as Selector.AnyLabelMatcher, call) >> Flux.just("{}")
0 * it.getEgressSubscription() 0 * it.getEgressSubscription()
} }

View File

@@ -16,17 +16,15 @@
*/ */
package io.emeraldpay.dshackle.rpc package io.emeraldpay.dshackle.rpc
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.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.test.GenericUpstreamMock
import io.emeraldpay.dshackle.test.MultistreamHolderMock import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeRpcUpstream
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
@@ -39,11 +37,9 @@ import java.time.Instant
class StreamHeadSpec extends Specification { class StreamHeadSpec extends Specification {
ObjectMapper objectMapper = Global.objectMapper
def "Errors on unavailable chain"() { def "Errors on unavailable chain"() {
setup: setup:
def upstreams = new MultistreamHolderMock(Chain.ETHEREUM__MAINNET, Stub(EthereumLikeRpcUpstream)) def upstreams = new MultistreamHolderMock(Chain.ETHEREUM__MAINNET, Stub(GenericUpstream))
def streamHead = new StreamHead(upstreams) def streamHead = new StreamHead(upstreams)
when: when:
def flux = streamHead.add( def flux = streamHead.add(
@@ -80,7 +76,7 @@ class StreamHeadSpec extends Specification {
.build() .build()
} }
def upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api()) def upstream = new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api())
def upstreams = new MultistreamHolderMock(Chain.ETHEREUM__MAINNET, upstream) def upstreams = new MultistreamHolderMock(Chain.ETHEREUM__MAINNET, upstream)
def streamHead = new StreamHead(upstreams) def streamHead = new StreamHead(upstreams)
when: when:

View File

@@ -1,87 +0,0 @@
/**
* Copyright (c) 2019 ETCDEV GmbH
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.*
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeRpcUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import org.jetbrains.annotations.NotNull
import io.emeraldpay.dshackle.foundation.ChainOptions
import org.reactivestreams.Publisher
class EthereumRpcUpstreamMock extends EthereumLikeRpcUpstream {
EthereumHeadMock ethereumHeadMock
static CallMethods allMethods() {
new AggregatedCallMethods([
new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET),
new DefaultBitcoinMethods(),
new DirectCallMethods(["eth_test"])
])
}
EthereumRpcUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(chain, api, allMethods())
}
EthereumRpcUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(id, chain, api, allMethods())
}
EthereumRpcUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
this("test", chain, api, methods)
}
EthereumRpcUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
super(id, id.hashCode().byteValue(), chain,
ChainOptions.PartialOptions.getDefaults().buildOptions(),
UpstreamsConfig.UpstreamRole.PRIMARY,
methods,
new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
new ConnectorFactoryMock(api, new EthereumHeadMock()),
ChainsConfig.ChainConfig.default(),
false,
null
)
this.ethereumHeadMock = this.getHead() as EthereumHeadMock
setLag(0)
setStatus(UpstreamAvailability.OK)
start()
}
void nextBlock(BlockContainer block) {
this.ethereumHeadMock.nextBlock(block)
}
void setBlocks(Publisher<BlockContainer> blocks) {
this.ethereumHeadMock.predefined = blocks
}
@Override
String toString() {
return "Upstream mock ${getId()}"
}
}

View File

@@ -2,7 +2,7 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnector
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -47,7 +47,7 @@ class GenericConnectorMock implements GenericConnector {
} }
@Override @Override
EthereumIngressSubscription getIngressSubscription() { IngressSubscription getIngressSubscription() {
return NoEthereumIngressSubscription.DEFAULT return NoEthereumIngressSubscription.DEFAULT
} }
} }

View File

@@ -24,14 +24,14 @@ import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.* import io.emeraldpay.dshackle.upstream.calls.*
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeRpcUpstream import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.foundation.ChainOptions
class EthereumPosRpcUpstreamMock extends EthereumLikeRpcUpstream { class GenericUpstreamMock extends GenericUpstream {
EthereumHeadMock ethereumHeadMock EthereumHeadMock ethereumHeadMock
static CallMethods allMethods() { static CallMethods allMethods() {
@@ -42,36 +42,40 @@ class EthereumPosRpcUpstreamMock extends EthereumLikeRpcUpstream {
]) ])
} }
EthereumPosRpcUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) { GenericUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(chain, api, allMethods()) this(chain, api, allMethods())
} }
EthereumPosRpcUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, Map<String, String> labels) { GenericUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, Map<String, String> labels) {
this(id, chain, api, allMethods(), labels) this(id, chain, api, allMethods(), labels)
} }
EthereumPosRpcUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) { GenericUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(id, chain, api, allMethods()) this(id, chain, api, allMethods())
} }
EthereumPosRpcUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) { GenericUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
this("test", chain, api, methods) this("test", chain, api, methods)
} }
EthereumPosRpcUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) { GenericUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
this(id, chain, api, methods, Collections.<String, String>emptyMap()) this(id, chain, api, methods, Collections.<String, String>emptyMap())
} }
EthereumPosRpcUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods, Map<String, String> labels) { GenericUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods, Map<String, String> labels) {
super(id, (byte)id.hashCode(), chain, super(id,
chain,
(byte)id.hashCode(),
getOpts(), getOpts(),
UpstreamsConfig.UpstreamRole.PRIMARY, UpstreamsConfig.UpstreamRole.PRIMARY,
methods, methods,
new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(labels)), new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(labels)),
new ConnectorFactoryMock(api, new EthereumHeadMock()),
ChainConfig.default(), ChainConfig.default(),
true, new ConnectorFactoryMock(api, new EthereumHeadMock()),
null null,
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&validator,
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&labelDetector,
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&subscriptionTopics,
) )
this.ethereumHeadMock = this.getHead() as EthereumHeadMock this.ethereumHeadMock = this.getHead() as EthereumHeadMock
setLag(0) setLag(0)

View File

@@ -20,13 +20,13 @@ import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.generic.*
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcUpstream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcUpstream
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeRpcUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
import org.springframework.cloud.sleuth.brave.bridge.BraveTracer import org.springframework.cloud.sleuth.brave.bridge.BraveTracer
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
@@ -42,13 +42,16 @@ class MultistreamHolderMock implements MultistreamHolder {
Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) { Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
if (!upstreams.containsKey(chain)) { if (!upstreams.containsKey(chain)) {
if (BlockchainType.from(chain) == BlockchainType.EVM_POS) { if (BlockchainType.from(chain) == BlockchainType.ETHEREUM) {
if (up instanceof EthereumPosMultiStream) { if (up instanceof GenericMultistream) {
upstreams[chain] = up upstreams[chain] = up
} else if (up instanceof EthereumLikeRpcUpstream) { } else if (up instanceof GenericUpstream) {
upstreams[chain] = new EthereumPosMultiStream( upstreams[chain] = new GenericMultistream(
chain, [up as EthereumLikeRpcUpstream], Caches.default(), chain, [up as GenericUpstream], Caches.default(),
Schedulers.boundedElastic(), TestingCommons.tracerMock() Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(TestingCommons.tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic())
) )
} else { } else {
throw new IllegalArgumentException("Unsupported upstream type ${up.class}") throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
@@ -90,21 +93,24 @@ class MultistreamHolderMock implements MultistreamHolder {
return upstreams.values().toList() return upstreams.values().toList()
} }
static class EthereumMultistreamMock extends EthereumPosMultiStream { static class EthereumMultistreamMock extends GenericMultistream {
EthereumCachingReader customReader = null EthereumCachingReader customReader = null
CallMethods customMethods = null CallMethods customMethods = null
Head customHead = null Head customHead = null
EthereumMultistreamMock(@NotNull Chain chain, @NotNull List<EthereumLikeRpcUpstream> upstreams, @NotNull Caches caches) { EthereumMultistreamMock(@NotNull Chain chain, @NotNull List<GenericUpstream> upstreams, @NotNull Caches caches) {
super(chain, upstreams, caches, Schedulers.boundedElastic(), new BraveTracer(null, null, null)) super(chain, upstreams, caches, Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(new BraveTracer(null, null, null)),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
} }
EthereumMultistreamMock(@NotNull Chain chain, @NotNull List<EthereumLikeRpcUpstream> upstreams) { EthereumMultistreamMock(@NotNull Chain chain, @NotNull List<GenericUpstream> upstreams) {
this(chain, upstreams, Caches.default()) this(chain, upstreams, Caches.default())
} }
EthereumMultistreamMock(@NotNull Chain chain, @NotNull EthereumLikeRpcUpstream upstream) { EthereumMultistreamMock(@NotNull Chain chain, @NotNull GenericUpstream upstream) {
this(chain, [upstream]) this(chain, [upstream])
} }

View File

@@ -28,10 +28,10 @@ import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.Multistream
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.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.dshackle.upstream.generic.GenericMultistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.etherjar.domain.TransactionId import io.emeraldpay.etherjar.domain.TransactionId
@@ -54,44 +54,49 @@ class TestingCommons {
return new TracerMock(null, null, null) return new TracerMock(null, null, null)
} }
static EthereumPosRpcUpstreamMock upstream() { static GenericUpstreamMock upstream() {
return new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, api()) return new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, api())
} }
static EthereumPosRpcUpstreamMock upstream(String id) { static GenericUpstreamMock upstream(String id) {
return new EthereumPosRpcUpstreamMock(id, Chain.ETHEREUM__MAINNET, api()) return new GenericUpstreamMock(id, Chain.ETHEREUM__MAINNET, api())
} }
static EthereumPosRpcUpstreamMock upstream(String id, String provider) { static GenericUpstreamMock upstream(String id, String provider) {
return new EthereumPosRpcUpstreamMock(id, Chain.ETHEREUM__MAINNET, api(), Collections.singletonMap("provider", provider)) return new GenericUpstreamMock(id, Chain.ETHEREUM__MAINNET, api(), Collections.singletonMap("provider", provider))
} }
static EthereumPosRpcUpstreamMock upstream(String id, Reader<JsonRpcRequest, JsonRpcResponse> api) { static GenericUpstreamMock upstream(String id, Reader<JsonRpcRequest, JsonRpcResponse> api) {
return new EthereumPosRpcUpstreamMock(id, Chain.ETHEREUM__MAINNET, api) return new GenericUpstreamMock(id, Chain.ETHEREUM__MAINNET, api)
} }
static EthereumPosRpcUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api) { static GenericUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
return new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, api) return new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, api)
} }
static EthereumPosRpcUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, String method) { static GenericUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, String method) {
return upstream(api, [method]) return upstream(api, [method])
} }
static EthereumPosRpcUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, List<String> methods) { static GenericUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, List<String> methods) {
return new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, api, new DirectCallMethods(methods)) return new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, api, new DirectCallMethods(methods))
} }
static EthereumPosRpcUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods callMethods) { static GenericUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods callMethods) {
return new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, api, callMethods) return new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, api, callMethods)
} }
static Multistream multistream(Reader<JsonRpcRequest, JsonRpcResponse> api) { static Multistream multistream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
return multistream(upstream(api)) return multistream(upstream(api))
} }
static Multistream multistream(EthereumPosRpcUpstreamMock up) { static Multistream multistream(GenericUpstreamMock up) {
return new EthereumPosMultiStream(Chain.ETHEREUM__MAINNET, [up], Caches.default(), Schedulers.boundedElastic(), tracerMock()).tap { return new GenericMultistream(Chain.ETHEREUM__MAINNET, [up], Caches.default(),
Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()),
).tap {
start() start()
} }
} }
@@ -112,11 +117,17 @@ class TestingCommons {
} }
static Multistream multistreamWithoutUpstreams(Chain chain) { static Multistream multistreamWithoutUpstreams(Chain chain) {
return new EthereumPosMultiStream(chain, [], emptyCaches().getCaches(chain), Schedulers.boundedElastic(), tracerMock()) return new GenericMultistream(chain, [], emptyCaches().getCaches(chain), Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
} }
static Multistream multistreamClassicWithoutUpstreams(Chain chain) { static Multistream multistreamClassicWithoutUpstreams(Chain chain) {
return new EthereumMultistream(chain, [], emptyCaches().getCaches(chain), Schedulers.boundedElastic(), tracerMock()) return new GenericMultistream(chain, [], emptyCaches().getCaches(chain), Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
} }
static FileResolver fileResolver() { static FileResolver fileResolver() {

View File

@@ -1,253 +0,0 @@
package io.emeraldpay.dshackle.upstream
import kotlin.jvm.functions.Function1
import org.jetbrains.annotations.NotNull
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import spock.lang.Specification
import java.time.Duration
import java.util.function.Function
class AbstractChainFeesSpec extends Specification {
Function<List<String>, List<String>> extractTx = { it }
def "Iterates N last blocks"() {
setup:
def ups = Mock(Multistream) {
1 * getHead() >> Mock(Head) {
1 * it.getCurrentHeight() >> 234
}
}
def fees = new TestChainFees(5, ups, Stub(Function1))
when:
def act = fees.usingBlocks(3).collectList().block(Duration.ofSeconds(1)).toSorted()
then:
act == [232L, 233L, 234L]
}
def "Limits iteration to configured"() {
setup:
def ups = Mock(Multistream) {
1 * getHead() >> Mock(Head) {
1 * it.getCurrentHeight() >> 234
}
}
def fees = new TestChainFees(3, ups, Stub(Function1))
when:
def act = fees.usingBlocks(5).collectList().block(Duration.ofSeconds(1)).toSorted()
then:
act == [232L, 233L, 234L]
}
def "TxAtPos 0 for empty list"() {
setup:
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 0)
when:
def act = txAt.get([])
then:
act == null
}
def "TxAtPos 0 for single item list"() {
setup:
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 0)
when:
def act = txAt.get(["t_1"])
then:
act == "t_1"
}
def "TxAtPos 0 for many item list"() {
setup:
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 0)
when:
def act = txAt.get(["t_1", "t_2", "t_3"])
then:
act == "t_3"
}
def "TxAtPos 1 for many item list"() {
setup:
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 1)
when:
def act = txAt.get(["t_1", "t_2", "t_3"])
then:
act == "t_2"
}
def "TxAtPos 2 for many item list"() {
setup:
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 2)
when:
def act = txAt.get(["t_1", "t_2", "t_3"])
then:
act == "t_1"
}
def "TxAtPos 5 for 3 item list"() {
setup:
def txAt = new AbstractChainFees.TxAtPos<List<String>, String>(extractTx, 5)
when:
def act = txAt.get(["t_1", "t_2", "t_3"])
then:
act == "t_1"
}
def "TxAtBottom for empty list"() {
setup:
def txAt = new AbstractChainFees.TxAtBottom<List<String>, String>(extractTx)
when:
def act = txAt.get([])
then:
act == null
}
def "TxAtBottom for single item list"() {
setup:
def txAt = new AbstractChainFees.TxAtBottom<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1"])
then:
act == "t_1"
}
def "TxAtBottom for multi item list"() {
setup:
def txAt = new AbstractChainFees.TxAtBottom<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1", "t_2", "t_3"])
then:
act == "t_3"
}
def "TxAtTop for empty list"() {
setup:
def txAt = new AbstractChainFees.TxAtTop<List<String>, String>(extractTx)
when:
def act = txAt.get([])
then:
act == null
}
def "TxAtTop for single item list"() {
setup:
def txAt = new AbstractChainFees.TxAtTop<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1"])
then:
act == "t_1"
}
def "TxAtTop for multi item list"() {
setup:
def txAt = new AbstractChainFees.TxAtTop<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1", "t_2", "t_3"])
then:
act == "t_1"
}
def "TxAtMiddle for empty list"() {
setup:
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
when:
def act = txAt.get([])
then:
act == null
}
def "TxAtMiddle for single item list"() {
setup:
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1"])
then:
act == "t_1"
}
def "TxAtMiddle for 3 item list"() {
setup:
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1", "t_2", "t_3"])
then:
act == "t_2"
}
def "TxAtMiddle for 4 item list"() {
setup:
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1", "t_2", "t_3", "t_4"])
then:
act == "t_2" || act == "t_3"
}
def "TxAtMiddle for 5 item list"() {
setup:
def txAt = new AbstractChainFees.TxAtMiddle<List<String>, String>(extractTx)
when:
def act = txAt.get(["t_1", "t_2", "t_3", "t_4", "t_5"])
then:
act == "t_3"
}
class TestChainFees extends AbstractChainFees {
TestChainFees(int heightLimit, @NotNull Multistream upstreams, @NotNull Function1 extractTx) {
super(heightLimit, upstreams, extractTx)
}
@Override
Mono readFeesAt(long height, @NotNull TxAt selector) {
return null
}
@Override
Function<Flux, Mono> feeAggregation(@NotNull Mode mode) {
return null
}
@Override
Function getResponseBuilder() {
return null
}
}
}

View File

@@ -17,8 +17,7 @@ package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock import io.emeraldpay.dshackle.test.GenericUpstreamMock
import io.emeraldpay.dshackle.test.EthereumRpcUpstreamMock
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import spock.lang.Specification import spock.lang.Specification
@@ -27,7 +26,7 @@ class CurrentMultistreamHolderSpec extends Specification {
def "add upstream"() { def "add upstream"() {
setup: setup:
def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams()) def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams())
def up = new EthereumPosRpcUpstreamMock("test", Chain.ETHEREUM__MAINNET, TestingCommons.api()) def up = new GenericUpstreamMock("test", Chain.ETHEREUM__MAINNET, TestingCommons.api())
when: when:
current.getUpstream(Chain.ETHEREUM__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up, UpstreamChangeEvent.ChangeType.ADDED)) current.getUpstream(Chain.ETHEREUM__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up, UpstreamChangeEvent.ChangeType.ADDED))
then: then:
@@ -38,9 +37,9 @@ class CurrentMultistreamHolderSpec extends Specification {
def "add multiple upstreams"() { def "add multiple upstreams"() {
setup: setup:
def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams()) def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams())
def up1 = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api()) def up1 = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api())
def up2 = new EthereumRpcUpstreamMock("test2", Chain.ETHEREUM_CLASSIC__MAINNET, TestingCommons.api()) def up2 = new GenericUpstreamMock("test2", Chain.ETHEREUM_CLASSIC__MAINNET, TestingCommons.api())
def up3 = new EthereumPosRpcUpstreamMock("test3", Chain.ETHEREUM__MAINNET, TestingCommons.api()) def up3 = new GenericUpstreamMock("test3", Chain.ETHEREUM__MAINNET, TestingCommons.api())
when: when:
current.getUpstream(Chain.ETHEREUM__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.ADDED)) current.getUpstream(Chain.ETHEREUM__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.ADDED))
current.getUpstream(Chain.ETHEREUM_CLASSIC__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM_CLASSIC__MAINNET, up2, UpstreamChangeEvent.ChangeType.ADDED)) current.getUpstream(Chain.ETHEREUM_CLASSIC__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM_CLASSIC__MAINNET, up2, UpstreamChangeEvent.ChangeType.ADDED))
@@ -55,10 +54,10 @@ class CurrentMultistreamHolderSpec extends Specification {
def "remove upstream"() { def "remove upstream"() {
setup: setup:
def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams()) def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams())
def up1 = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api()) def up1 = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api())
def up2 = new EthereumRpcUpstreamMock("test2", Chain.ETHEREUM_CLASSIC__MAINNET, TestingCommons.api()) def up2 = new GenericUpstreamMock("test2", Chain.ETHEREUM_CLASSIC__MAINNET, TestingCommons.api())
def up3 = new EthereumPosRpcUpstreamMock("test3", Chain.ETHEREUM__MAINNET, TestingCommons.api()) def up3 = new GenericUpstreamMock("test3", Chain.ETHEREUM__MAINNET, TestingCommons.api())
def up1_del = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api()) def up1_del = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api())
when: when:
current.getUpstream(Chain.ETHEREUM__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.ADDED)) current.getUpstream(Chain.ETHEREUM__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.ADDED))
current.getUpstream(Chain.ETHEREUM_CLASSIC__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM_CLASSIC__MAINNET, up2, UpstreamChangeEvent.ChangeType.ADDED)) current.getUpstream(Chain.ETHEREUM_CLASSIC__MAINNET).onUpstreamChange(new UpstreamChangeEvent(Chain.ETHEREUM_CLASSIC__MAINNET, up2, UpstreamChangeEvent.ChangeType.ADDED))
@@ -73,7 +72,7 @@ class CurrentMultistreamHolderSpec extends Specification {
def "available after adding"() { def "available after adding"() {
setup: setup:
def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams()) def current = new CurrentMultistreamHolder(TestingCommons.defaultMultistreams())
def up1 = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api()) def up1 = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api())
when: when:
def act = current.isAvailable(Chain.ETHEREUM__MAINNET) def act = current.isAvailable(Chain.ETHEREUM__MAINNET)

View File

@@ -24,7 +24,7 @@ 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.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeRpcUpstream import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory import io.emeraldpay.dshackle.upstream.generic.connectors.GenericConnectorFactory
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
@@ -44,13 +44,15 @@ class FilteredApisSpec extends Specification {
def "Verifies labels"() { def "Verifies labels"() {
setup: setup:
def i = 0 def i = 0
List<EthereumLikeRpcUpstream> upstreams = [ def cs = io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE
List<GenericUpstream> upstreams = [
[test: "foo"], [test: "foo"],
[test: "bar"], [test: "bar"],
[test: "foo", test2: "baz"], [test: "foo", test2: "baz"],
[test: "foo"], [test: "foo"],
[test: "baz"] [test: "baz"]
].collect { ].collect {
def httpFactory = Mock(HttpFactory) { def httpFactory = Mock(HttpFactory) {
create(_, _) >> Stub(JsonRpcHttpReader) create(_, _) >> Stub(JsonRpcHttpReader)
} }
@@ -64,18 +66,20 @@ class FilteredApisSpec extends Specification {
Schedulers.boundedElastic(), Schedulers.boundedElastic(),
Duration.ofSeconds(12) Duration.ofSeconds(12)
) )
new EthereumLikeRpcUpstream( new GenericUpstream(
"test", "test",
(byte) 123,
Chain.ETHEREUM__MAINNET, Chain.ETHEREUM__MAINNET,
(byte) 123,
new ChainOptions.PartialOptions().buildOptions(), new ChainOptions.PartialOptions().buildOptions(),
UpstreamsConfig.UpstreamRole.PRIMARY, UpstreamsConfig.UpstreamRole.PRIMARY,
ethereumTargets, ethereumTargets,
new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(it)), new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(it)),
connectorFactory,
ChainsConfig.ChainConfig.default(), ChainsConfig.ChainConfig.default(),
false, connectorFactory,
null null,
cs.&validator,
cs.&labelDetector,
cs.&subscriptionTopics,
) )
} }
def matcher = new Selector.LabelMatcher("test", ["foo"]) def matcher = new Selector.LabelMatcher("test", ["foo"])

View File

@@ -25,15 +25,17 @@ import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock import io.emeraldpay.dshackle.test.GenericUpstreamMock
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.EthereumLikeRpcUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.generic.GenericMultistream
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.grpc.EthereumPosGrpcUpstream import io.emeraldpay.dshackle.upstream.grpc.GenericGrpcUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific
import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
@@ -51,9 +53,13 @@ class MultistreamSpec extends Specification {
def "Aggregates methods"() { def "Aggregates methods"() {
setup: setup:
def up1 = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2"])) def up1 = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2"]))
def up2 = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test2", "eth_test3"])) def up2 = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test2", "eth_test3"]))
def aggr = new EthereumPosMultiStream(Chain.ETHEREUM__MAINNET, [up1, up2], Caches.default(), Schedulers.boundedElastic(), TestingCommons.tracerMock()) def aggr = new GenericMultistream(Chain.ETHEREUM__MAINNET, [up1, up2], Caches.default(),
Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(TestingCommons.tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
when: when:
aggr.onUpstreamsUpdated() aggr.onUpstreamsUpdated()
def act = aggr.getMethods() def act = aggr.getMethods()
@@ -184,7 +190,11 @@ class MultistreamSpec extends Specification {
def up1 = TestingCommons.upstream("test-1", "internal") def up1 = TestingCommons.upstream("test-1", "internal")
def up2 = TestingCommons.upstream("test-2", "external") def up2 = TestingCommons.upstream("test-2", "external")
def up3 = TestingCommons.upstream("test-3", "external") def up3 = TestingCommons.upstream("test-3", "external")
def multistream = new EthereumPosMultiStream(Chain.ETHEREUM__MAINNET, [up1, up2, up3], Caches.default(), Schedulers.boundedElastic(), TestingCommons.tracerMock()) def multistream = new GenericMultistream(Chain.ETHEREUM__MAINNET, [up1, up2, up3], Caches.default(),
Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(TestingCommons.tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
expect: expect:
multistream.getHead(new Selector.LabelMatcher("provider", ["internal"])).is(up1.ethereumHeadMock) multistream.getHead(new Selector.LabelMatcher("provider", ["internal"])).is(up1.ethereumHeadMock)
@@ -205,14 +215,14 @@ class MultistreamSpec extends Specification {
.setMethod("newHeads") .setMethod("newHeads")
.build() .build()
def up1 = Mock(EthereumPosGrpcUpstream) { def up1 = Mock(GenericGrpcUpstream) {
1 * isGrpc() >> true 1 * isGrpc() >> true
1 * getId() >> "internal" _ * getId() >> "internal"
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(Collections.singletonMap("provider", "internal"))] 1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(Collections.singletonMap("provider", "internal"))]
1 * proxySubscribe(call) >> Flux.just("{}") 1 * proxySubscribe(call) >> Flux.just("{}")
} }
def up2 = Mock(EthereumPosGrpcUpstream) { def up2 = Mock(GenericUpstream) {
1 * getId() >> "external" _ * getId() >> "external"
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(Collections.singletonMap("provider", "external"))] 1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(Collections.singletonMap("provider", "external"))]
} }
def multiStream = new TestEthereumPosMultistream(Chain.ETHEREUM__MAINNET, [up1, up2], Caches.default()) def multiStream = new TestEthereumPosMultistream(Chain.ETHEREUM__MAINNET, [up1, up2], Caches.default())
@@ -235,9 +245,9 @@ class MultistreamSpec extends Specification {
.setMethod("newHeads") .setMethod("newHeads")
.build() .build()
def up2 = Mock(EthereumPosGrpcUpstream) { def up2 = Mock(GenericUpstream) {
1 * isGrpc() >> false 1 * isGrpc() >> false
1 * getId() >> "2" _ * getId() >> "2"
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(Collections.singletonMap("provider", "internal"))] 1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(Collections.singletonMap("provider", "internal"))]
} }
def multiStream = new TestEthereumPosMultistream(Chain.ETHEREUM__MAINNET, [up2], Caches.default()) def multiStream = new TestEthereumPosMultistream(Chain.ETHEREUM__MAINNET, [up2], Caches.default())
@@ -251,9 +261,13 @@ class MultistreamSpec extends Specification {
def "Change ms methods based on upstream availability"() { def "Change ms methods based on upstream availability"() {
setup: setup:
def up1 = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2", "eth_test3"])) def up1 = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2", "eth_test3"]))
def up2 = new EthereumPosRpcUpstreamMock("test2", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2"])) def up2 = new GenericUpstreamMock("test2", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2"]))
def ms = new EthereumPosMultiStream(Chain.ETHEREUM__MAINNET, new ArrayList<EthereumLikeRpcUpstream>(), Caches.default(), Schedulers.boundedElastic(), TestingCommons.tracerMock()) def ms = new GenericMultistream(Chain.ETHEREUM__MAINNET, new ArrayList<GenericMultistream>(), Caches.default(),
Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(TestingCommons.tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
when: when:
ms.onUpstreamChange( ms.onUpstreamChange(
new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.ADDED) new UpstreamChangeEvent(Chain.ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.ADDED)
@@ -278,9 +292,13 @@ class MultistreamSpec extends Specification {
def "Filter older blocks on multistream head"() { def "Filter older blocks on multistream head"() {
setup: setup:
def up1 = new EthereumPosRpcUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2", "eth_test3"])) def up1 = new GenericUpstreamMock("test1", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2", "eth_test3"]))
def up2 = new EthereumPosRpcUpstreamMock("test2", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2"])) def up2 = new GenericUpstreamMock("test2", Chain.ETHEREUM__MAINNET, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2"]))
def ms = new EthereumPosMultiStream(Chain.ETHEREUM__MAINNET, new ArrayList<EthereumLikeRpcUpstream>(), Caches.default(), Schedulers.boundedElastic(), TestingCommons.tracerMock()) def ms = new GenericMultistream(Chain.ETHEREUM__MAINNET, new ArrayList<GenericMultistream>(), Caches.default(),
Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(TestingCommons.tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
def head1 = createBlock(250, "0x0d050c785de17179f935b9b93aca09c442964cc59972c71ae68e74731448401b") def head1 = createBlock(250, "0x0d050c785de17179f935b9b93aca09c442964cc59972c71ae68e74731448401b")
def head2 = createBlock(270, "0x0d050c785de17179f935b9b93aca09c442964cc59972c71ae68e74731448402b") def head2 = createBlock(270, "0x0d050c785de17179f935b9b93aca09c442964cc59972c71ae68e74731448402b")
def head3 = createBlock(100, "0x0d050c785de17179f935b9b93aca09c442964cc59972c71ae68e74731448412b") def head3 = createBlock(100, "0x0d050c785de17179f935b9b93aca09c442964cc59972c71ae68e74731448412b")
@@ -311,7 +329,11 @@ class MultistreamSpec extends Specification {
def up1 = TestingCommons.upstream("test-1", "internal") def up1 = TestingCommons.upstream("test-1", "internal")
def up2 = TestingCommons.upstream("test-2", "external") def up2 = TestingCommons.upstream("test-2", "external")
def up3 = TestingCommons.upstream("test-3", "external") def up3 = TestingCommons.upstream("test-3", "external")
def multistream = new EthereumPosMultiStream(Chain.ETHEREUM__MAINNET, [up1, up2, up3], Caches.default(), Schedulers.boundedElastic(), TestingCommons.tracerMock()) def multistream = new GenericMultistream(Chain.ETHEREUM__MAINNET, [up1, up2, up3], Caches.default(),
Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(TestingCommons.tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
EthereumChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
def observer = multistream.lagObserver def observer = multistream.lagObserver
multistream.onUpstreamsUpdated() multistream.onUpstreamsUpdated()
@@ -335,15 +357,19 @@ class MultistreamSpec extends Specification {
.build() .build()
} }
class TestEthereumPosMultistream extends EthereumPosMultiStream { class TestEthereumPosMultistream extends GenericMultistream {
TestEthereumPosMultistream(@NotNull Chain chain, @NotNull List<EthereumLikeRpcUpstream> upstreams, @NotNull Caches caches) { TestEthereumPosMultistream(@NotNull Chain chain, @NotNull List<GenericUpstream> upstreams, @NotNull Caches caches) {
super(chain, upstreams, caches, Schedulers.boundedElastic(), TestingCommons.tracerMock()) super(chain, upstreams, caches,
Schedulers.boundedElastic(),
EthereumChainSpecific.INSTANCE.makeCachingReaderBuilder(TestingCommons.tracerMock()),
EthereumChainSpecific.INSTANCE.&localReaderBuilder,
StarknetChainSpecific.INSTANCE.subscriptionBuilder(Schedulers.boundedElastic()))
} }
@NotNull @NotNull
@Override @Override
Mono<Reader<JsonRpcRequest, JsonRpcResponse>> getLocalReader(boolean localEnabled) { Mono<Reader<JsonRpcRequest, JsonRpcResponse>> getLocalReader() {
return null return null
} }

View File

@@ -1,91 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock
import io.emeraldpay.dshackle.test.ReaderMock
import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.FilteredApis
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.erc20.ERC20Token
import io.emeraldpay.etherjar.hex.HexData
import io.emeraldpay.etherjar.rpc.json.TransactionCallJson
import spock.lang.Specification
import java.time.Duration
class ERC20BalanceSpec extends Specification {
def "Gets balance from upstream"() {
setup:
ReaderMock api = new ReaderMock()
.with(
new JsonRpcRequest("eth_call", [
new TransactionCallJson().tap { json ->
json.setTo(Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9"))
json.setData(HexData.from("0x70a0823100000000000000000000000016c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"))
},
"latest"
]),
JsonRpcResponse.ok('"0x0000000000000000000000000000000000000000000000000000001f28d72868"')
)
EthereumLikeRpcUpstream upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, api)
ERC20Token token = new ERC20Token(Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9"))
ERC20Balance query = new ERC20Balance()
when:
def act = query.getBalance(upstream, token, Address.from("0x16c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"))
.block(Duration.ofSeconds(1))
then:
act.toLong() == 0x1f28d72868
}
def "Gets balance from api source"() {
setup:
ReaderMock api = new ReaderMock()
.with(
new JsonRpcRequest("eth_call", [
new TransactionCallJson().tap { json ->
json.setTo(Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9"))
json.setData(HexData.from("0x70a0823100000000000000000000000016c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"))
},
"latest"
]),
JsonRpcResponse.ok('"0x0000000000000000000000000000000000000000000000000000001f28d72868"')
)
EthereumLikeRpcUpstream upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, api)
ERC20Token token = new ERC20Token(Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9"))
ERC20Balance query = new ERC20Balance()
ApiSource apiSource = new FilteredApis(
Chain.ETHEREUM__MAINNET, [upstream], Selector.empty
)
when:
def act = query.getBalance(apiSource, token, Address.from("0x16c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"))
.block(Duration.ofSeconds(1))
then:
act.toLong() == 0x1f28d72868
}
}

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.generic.GenericMultistream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.etherjar.domain.Address import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.hex.Hex32 import io.emeraldpay.etherjar.hex.Hex32
@@ -28,7 +29,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "read empty logs request"() { def "read empty logs request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([:]) def act = ethereumSubscribe.readLogsRequest([:])
@@ -39,7 +40,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "read single address logs request"() { def "read single address logs request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([ def act = ethereumSubscribe.readLogsRequest([
address: "0x829bd824b016326a401d083b33d092293333a830" address: "0x829bd824b016326a401d083b33d092293333a830"
@@ -64,7 +65,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "ignores invalid address for logs request"() { def "ignores invalid address for logs request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([ def act = ethereumSubscribe.readLogsRequest([
address: "829bd824b016326a401d083b33d092293333a830" address: "829bd824b016326a401d083b33d092293333a830"
@@ -77,7 +78,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "read multi address logs request"() { def "read multi address logs request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([ def act = ethereumSubscribe.readLogsRequest([
address: ["0x829bd824b016326a401d083b33d092293333a830", "0x401d083b33d092293333a83829bd824b016326a0"] address: ["0x829bd824b016326a401d083b33d092293333a830", "0x401d083b33d092293333a83829bd824b016326a0"]
@@ -93,7 +94,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "read single topic logs request"() { def "read single topic logs request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([ def act = ethereumSubscribe.readLogsRequest([
topics: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" topics: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
@@ -118,7 +119,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "read invalid topic for request"() { def "read invalid topic for request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([ def act = ethereumSubscribe.readLogsRequest([
topics: [ topics: [
@@ -136,7 +137,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "read multi topic logs request"() { def "read multi topic logs request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([ def act = ethereumSubscribe.readLogsRequest([
topics: [ topics: [
@@ -155,7 +156,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def "read full logs request"() { def "read full logs request"() {
setup: setup:
def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe = new EthereumEgressSubscription(TestingCommons.emptyMultistream() as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
when: when:
def act = ethereumSubscribe.readLogsRequest([ def act = ethereumSubscribe.readLogsRequest([
address: "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695", address: "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",
@@ -180,7 +181,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
def up1 = TestingCommons.upstream("test") def up1 = TestingCommons.upstream("test")
up1.getConnectorMock().setLiveness(Flux.just(false)) up1.getConnectorMock().setLiveness(Flux.just(false))
def ethereumSubscribe1 = new EthereumEgressSubscription(TestingCommons.multistream(up1) as EthereumPosMultiStream, Schedulers.boundedElastic(), null) def ethereumSubscribe1 = new EthereumEgressSubscription(TestingCommons.multistream(up1) as GenericMultistream, Schedulers.boundedElastic(), null)
then: then:
ethereumSubscribe1.getAvailableTopics() == [] ethereumSubscribe1.getAvailableTopics() == []
when: when:
@@ -188,7 +189,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
up2.getConnectorMock().setLiveness(Flux.just(true)) up2.getConnectorMock().setLiveness(Flux.just(true))
up2.stop() up2.stop()
up2.start() up2.start()
def ethereumSubscribe2 = new EthereumEgressSubscription(TestingCommons.multistream(up2) as EthereumPosMultiStream, Schedulers.boundedElastic(), null) def ethereumSubscribe2 = new EthereumEgressSubscription(TestingCommons.multistream(up2) as GenericMultistream, Schedulers.boundedElastic(), null)
then: then:
ethereumSubscribe2.getAvailableTopics().toSet() == [EthereumEgressSubscription.METHOD_LOGS, EthereumEgressSubscription.METHOD_NEW_HEADS].toSet() ethereumSubscribe2.getAvailableTopics().toSet() == [EthereumEgressSubscription.METHOD_LOGS, EthereumEgressSubscription.METHOD_NEW_HEADS].toSet()
when: when:
@@ -196,7 +197,7 @@ class EthereumEgressSubscriptionSpec extends Specification {
up3.getConnectorMock().setLiveness(Flux.just(true)) up3.getConnectorMock().setLiveness(Flux.just(true))
up3.stop() up3.stop()
up3.start() up3.start()
def ethereumSubscribe3 = new EthereumEgressSubscription(TestingCommons.multistream(up3) as EthereumPosMultiStream, Schedulers.boundedElastic(), Stub(PendingTxesSource)) def ethereumSubscribe3 = new EthereumEgressSubscription(TestingCommons.multistream(up3) as GenericMultistream, Schedulers.boundedElastic(), Stub(PendingTxesSource))
then: then:
ethereumSubscribe3.getAvailableTopics().toSet() == [EthereumEgressSubscription.METHOD_LOGS, EthereumEgressSubscription.METHOD_NEW_HEADS, EthereumEgressSubscription.METHOD_PENDING_TXES].toSet() ethereumSubscribe3.getAvailableTopics().toSet() == [EthereumEgressSubscription.METHOD_LOGS, EthereumEgressSubscription.METHOD_NEW_HEADS, EthereumEgressSubscription.METHOD_PENDING_TXES].toSet()

View File

@@ -1,41 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
import io.emeraldpay.etherjar.domain.Wei
import spock.lang.Specification
class EthereumLegacyFeesSpec extends Specification {
def "Extract fee from"() {
setup:
def block = new BlockJson()
// 0x75cc01873a9818bf426a8b23d83450bf18530a822fd4fe9e86a416a5554176a6
def tx = new TransactionJsonSnapshot().tap {
it.gasPrice = Wei.ofUnits(8, Wei.Unit.GWEI)
}
def fees = new EthereumLegacyFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = fees.extractFee(block, tx)
then:
act.priority == Wei.ofUnits(8, Wei.Unit.GWEI)
act.max == Wei.ofUnits(8, Wei.Unit.GWEI)
act.base == Wei.ZERO
}
}

View File

@@ -30,8 +30,7 @@ class EthereumLocalReaderSpec extends Specification {
TestingCommons.tracerMock() TestingCommons.tracerMock()
), ),
methods, methods,
new EmptyHead(), new EmptyHead()
true
) )
when: when:
def act = router.read(new JsonRpcRequest("eth_coinbase", [])).block(Duration.ofSeconds(1)) def act = router.read(new JsonRpcRequest("eth_coinbase", [])).block(Duration.ofSeconds(1))
@@ -50,8 +49,7 @@ class EthereumLocalReaderSpec extends Specification {
TestingCommons.tracerMock() TestingCommons.tracerMock()
), ),
methods, methods,
new EmptyHead(), new EmptyHead()
true
) )
when: when:
def act = router.read(new JsonRpcRequest("eth_getTransactionByHash", ["test"], 10)) def act = router.read(new JsonRpcRequest("eth_getTransactionByHash", ["test"], 10))
@@ -75,7 +73,7 @@ class EthereumLocalReaderSpec extends Specification {
} }
} }
def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET) def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET)
def router = new EthereumLocalReader(reader, methods, head, true) def router = new EthereumLocalReader(reader, methods, head)
when: when:
def act = router.getBlockByNumber(["latest", false]) def act = router.getBlockByNumber(["latest", false])
@@ -103,7 +101,7 @@ class EthereumLocalReaderSpec extends Specification {
} }
} }
def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET) def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET)
def router = new EthereumLocalReader(reader, methods, head, true) def router = new EthereumLocalReader(reader, methods, head)
when: when:
def act = router.getBlockByNumber(["earliest", false]) def act = router.getBlockByNumber(["earliest", false])
@@ -131,7 +129,7 @@ class EthereumLocalReaderSpec extends Specification {
} }
} }
def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET) def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET)
def router = new EthereumLocalReader(reader, methods, head, true) def router = new EthereumLocalReader(reader, methods, head)
when: when:
def act = router.getBlockByNumber(["0x123ef", false]) def act = router.getBlockByNumber(["0x123ef", false])
@@ -155,7 +153,7 @@ class EthereumLocalReaderSpec extends Specification {
_ * blocksByHeightAsCont() >> new EmptyReader<>() _ * blocksByHeightAsCont() >> new EmptyReader<>()
} }
def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET) def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET)
def router = new EthereumLocalReader(reader, methods, head, true) def router = new EthereumLocalReader(reader, methods, head)
when: when:
def act = router.getBlockByNumber(["0x0", true]) def act = router.getBlockByNumber(["0x0", true])

View File

@@ -1,150 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import spock.lang.Specification
import java.time.Duration
class EthereumPriorityFeesSpec extends Specification {
def "Extract fee from EIP1559 tx"() {
setup:
// 13756007
def block = new BlockJson().tap {
it.baseFeePerGas = new Wei(104197355513)
}
// 0x5da50f35a51e56ecd4313417b1c30f9c088222f3f8763701effe14f3dd18b6cc
def tx = new TransactionJsonSnapshot().tap {
it.type = 2
it.maxFeePerGas = Wei.ofUnits(999, Wei.Unit.GWEI)
it.maxPriorityFeePerGas = Wei.ofUnits(5.0001, Wei.Unit.GWEI)
}
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = fees.extractFee(block, tx)
then:
act.priority == Wei.ofUnits(5.0001, Wei.Unit.GWEI)
act.max == Wei.ofUnits(999, Wei.Unit.GWEI)
act.base == new Wei(104197355513)
}
def "Extract fee from legacy tx"() {
setup:
// 13756007
def block = new BlockJson().tap {
it.baseFeePerGas = new Wei(104197355513)
}
// 0x1f507982bef0f11a8304287d41f228b5f1dda1114a446ee781c3d95ef4a7b891
def tx = new TransactionJsonSnapshot().tap {
it.type = 0
// 109.564020111 Gwei
it.gasPrice = Wei.from("0x198286458f")
}
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = fees.extractFee(block, tx)
then:
// as difference between base minimum and actually paid
act.priority == Wei.ofUnits(5.366664598, Wei.Unit.GWEI)
act.max == Wei.from("0x198286458f")
act.base == new Wei(104197355513)
}
def "Calculates average fee"() {
setup:
def inputs = [
new EthereumFees.EthereumFee(Wei.ofEthers(1), Wei.ofEthers(0.5), Wei.ofEthers(0.75), Wei.ofEthers(0.5)),
new EthereumFees.EthereumFee(Wei.ofEthers(0.75), Wei.ofEthers(0.5), Wei.ofEthers(0.75), Wei.ofEthers(0.5)),
new EthereumFees.EthereumFee(Wei.ofEthers(0.6), Wei.ofEthers(0.2), Wei.ofEthers(0.6), Wei.ofEthers(0.5)),
]
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = Flux.fromIterable(inputs)
.transform(fees.feeAggregation(ChainFees.Mode.AVG_LAST))
.next().block(Duration.ofSeconds(1))
then:
act.priority == Wei.ofEthers(0.4) // 0.5 + 0.5 + 0.2
act.paid == Wei.ofEthers(0.7) // 0.75 + 0.75 + 0.6
}
def "Estimate"() {
setup:
def block1 = new BlockJson().tap {
it.baseFeePerGas = new Wei(92633661632)
it.transactions = [
new TransactionRefJson(TransactionId.from("0x00000000fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
new TransactionRefJson(TransactionId.from("0x11111111fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
new TransactionRefJson(TransactionId.from("0x22222222fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
]
}
def block2 = new BlockJson().tap {
it.baseFeePerGas = new Wei(104197355513)
it.transactions = [
new TransactionRefJson(TransactionId.from("0x33333333fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
new TransactionRefJson(TransactionId.from("0x44444444fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
new TransactionRefJson(TransactionId.from("0x55555555fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")),
]
}
def tx1 = new TransactionJsonSnapshot().tap {
it.type = 2
it.maxFeePerGas = Wei.ofUnits(150, Wei.Unit.GWEI)
it.maxPriorityFeePerGas = Wei.ofUnits(3, Wei.Unit.GWEI)
}
def tx2 = new TransactionJsonSnapshot().tap {
it.type = 2
it.maxFeePerGas = Wei.ofUnits(200, Wei.Unit.GWEI)
it.maxPriorityFeePerGas = Wei.ofUnits(6, Wei.Unit.GWEI)
}
def ups = Mock(EthereumMultistream) {
1 * getHead() >> Mock(Head) {
1 * getCurrentHeight() >> 13756007
}
}
def reader = Mock(EthereumCachingReader) {
_ * it.blocksByHeightParsed() >> Mock(Reader) {
1 * it.read(13756006) >> Mono.just(block1)
1 * it.read(13756007) >> Mono.just(block2)
}
_ * it.txByHash() >> Mock(Reader) {
1 * it.read(TransactionId.from("0x22222222fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")) >> Mono.just(tx1)
1 * it.read(TransactionId.from("0x55555555fad596cad644b785a8a74f6580ceec9ae13c8aa174f819c0223b8c77")) >> Mono.just(tx2)
}
}
def fees = new EthereumPriorityFees(ups, reader, 10)
when:
def act = fees.estimate(ChainFees.Mode.AVG_LAST, 2).block(Duration.ofSeconds(1))
then:
act.hasEthereumExtended()
act.ethereumExtended.priority == "4500000000"
act.ethereumExtended.max == "175000000000"
act.ethereumExtended.expect == "102915508572"
}
}

View File

@@ -24,6 +24,7 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.etherjar.domain.Address import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.hex.HexData import io.emeraldpay.etherjar.hex.HexData
import io.emeraldpay.etherjar.rpc.RpcResponseError import io.emeraldpay.etherjar.rpc.RpcResponseError
@@ -37,16 +38,20 @@ import java.time.Duration
import static io.emeraldpay.dshackle.Chain.ETHEREUM__MAINNET import static io.emeraldpay.dshackle.Chain.ETHEREUM__MAINNET
import static io.emeraldpay.dshackle.Chain.OPTIMISM__MAINNET import static io.emeraldpay.dshackle.Chain.OPTIMISM__MAINNET
import static io.emeraldpay.dshackle.upstream.UpstreamAvailability.* import static io.emeraldpay.dshackle.upstream.UpstreamAvailability.*
import static io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR import static io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR
import static io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR import static io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR
import static io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator.ValidateUpstreamSettingsResult.UPSTREAM_VALID import static io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult.UPSTREAM_VALID
import static java.util.Collections.emptyList import static java.util.Collections.emptyList
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
class EthereumUpstreamValidatorSpec extends Specification { class EthereumUpstreamValidatorSpec extends Specification {
def conf = ChainConfig.defaultWithContract("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96")
def "Resolve to final availability"() { def "Resolve to final availability"() {
setup: setup:
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, Stub(EthereumLikeUpstream), ChainOptions.PartialOptions.getDefaults().buildOptions())
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, Stub(Upstream), ChainOptions.PartialOptions.getDefaults().buildOptions(), conf)
expect: expect:
validator.resolve(Tuples.of(sync, peers)) == exp validator.resolve(Tuples.of(sync, peers)) == exp
where: where:
@@ -67,8 +72,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
def options = ChainOptions.PartialOptions.getDefaults().tap { def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateSyncing = false it.validateSyncing = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) def up = Mock(Upstream)
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -87,7 +92,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("eth_syncing", [], false) answer("eth_syncing", [], false)
} }
) )
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -100,7 +105,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def options = ChainOptions.PartialOptions.getDefaults().tap { def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateSyncing = true it.validateSyncing = true
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) { def up = Mock(Upstream) {
2 * getIngressReader() >> Mock(Reader) { reader -> 2 * getIngressReader() >> Mock(Reader) { reader ->
2 * reader.read(_) >>> [ 2 * reader.read(_) >>> [
Mono.just(new JsonRpcResponse('true'.getBytes(), null)), Mono.just(new JsonRpcResponse('true'.getBytes(), null)),
@@ -112,7 +117,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
1 * head.onSyncingNode(false) 1 * head.onSyncingNode(false)
} }
} }
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -132,7 +137,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("eth_syncing", [], [startingBlock: 100, currentBlock: 50]) answer("eth_syncing", [], [startingBlock: 100, currentBlock: 50])
} }
) )
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -150,7 +155,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("eth_syncing", [], new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unavailable")) answer("eth_syncing", [], new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unavailable"))
} }
) )
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -164,8 +169,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
it.validatePeers = false it.validatePeers = false
it.minPeers = 10 it.minPeers = 10
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) def up = Mock(Upstream)
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -180,8 +185,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
it.validatePeers = true it.validatePeers = true
it.minPeers = 0 it.minPeers = 0
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) def up = Mock(Upstream)
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -201,7 +206,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("net_peerCount", [], "0x5") answer("net_peerCount", [], "0x5")
} }
) )
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -220,7 +225,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("net_peerCount", [], "0xa") answer("net_peerCount", [], "0xa")
} }
) )
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -239,7 +244,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("net_peerCount", [], "0xff") answer("net_peerCount", [], "0xff")
} }
) )
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -258,7 +263,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("net_peerCount", [], new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unavailable")) answer("net_peerCount", [], new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unavailable"))
} }
) )
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -272,7 +277,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
it.validateCalllimit = false it.validateCalllimit = false
it.validateChain = false it.validateChain = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) { def up = Mock(Upstream) {
2 * getIngressReader() >> 2 * getIngressReader() >>
Mock(Reader) { Mock(Reader) {
1 * read(new JsonRpcRequest("eth_blockNumber", [])) >> Mono.just(new JsonRpcResponse('"0x10ff9be"'.getBytes(), null)) 1 * read(new JsonRpcRequest("eth_blockNumber", [])) >> Mono.just(new JsonRpcResponse('"0x10ff9be"'.getBytes(), null))
@@ -280,7 +285,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new JsonRpcResponse('"result"'.getBytes(), null)) Mono.just(new JsonRpcResponse('"result"'.getBytes(), null))
} }
} }
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
when: when:
def act = validator.validateUpstreamSettingsOnStartup() def act = validator.validateUpstreamSettingsOnStartup()
@@ -293,7 +298,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def options = ChainOptions.PartialOptions.getDefaults().tap { def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateChain = false it.validateChain = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeRpcUpstream) { def up = Mock(Upstream) {
3 * getIngressReader() >> Mock(Reader) { 3 * getIngressReader() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_call", [new TransactionCallJson( 1 * read(new JsonRpcRequest("eth_call", [new TransactionCallJson(
Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"),
@@ -304,8 +309,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new JsonRpcResponse('"result"'.getBytes(), null)) Mono.just(new JsonRpcResponse('"result"'.getBytes(), null))
} }
} }
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
//"0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96
when: when:
def act = validator.validateUpstreamSettingsOnStartup() def act = validator.validateUpstreamSettingsOnStartup()
then: then:
@@ -317,7 +322,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def options = ChainOptions.PartialOptions.getDefaults().tap { def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateChain = false it.validateChain = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeRpcUpstream) { def up = Mock(Upstream) {
3 * getIngressReader() >> Mock(Reader) { 3 * getIngressReader() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_call", [new TransactionCallJson( 1 * read(new JsonRpcRequest("eth_call", [new TransactionCallJson(
Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"),
@@ -328,8 +333,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new JsonRpcResponse('"result"'.getBytes(), null)) Mono.just(new JsonRpcResponse('"result"'.getBytes(), null))
} }
} }
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
// "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"
when: when:
def act = validator.validateUpstreamSettingsOnStartup() def act = validator.validateUpstreamSettingsOnStartup()
then: then:
@@ -341,7 +346,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def options = ChainOptions.PartialOptions.getDefaults().tap { def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateCalllimit = false it.validateCalllimit = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeRpcUpstream) { def up = Mock(Upstream) {
4 * getIngressReader() >> Mock(Reader) { 4 * getIngressReader() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null)) 1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null))
1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null)) 1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null))
@@ -350,8 +355,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new JsonRpcResponse('"result"'.getBytes(), null)) Mono.just(new JsonRpcResponse('"result"'.getBytes(), null))
} }
} }
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
// "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"
when: when:
def act = validator.validateUpstreamSettingsOnStartup() def act = validator.validateUpstreamSettingsOnStartup()
then: then:
@@ -363,7 +368,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def options = ChainOptions.PartialOptions.getDefaults().tap { def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateCalllimit = false it.validateCalllimit = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeRpcUpstream) { def up = Mock(Upstream) {
4 * getIngressReader() >> Mock(Reader) { 4 * getIngressReader() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null)) 1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null))
1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null)) 1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null))
@@ -372,8 +377,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new JsonRpcResponse('"result"'.getBytes(), null)) Mono.just(new JsonRpcResponse('"result"'.getBytes(), null))
} }
} }
def validator = new EthereumUpstreamValidator(OPTIMISM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") def validator = new EthereumUpstreamValidator(OPTIMISM__MAINNET, up, options, conf)
// "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"
when: when:
def act = validator.validateUpstreamSettingsOnStartup() def act = validator.validateUpstreamSettingsOnStartup()
then: then:
@@ -383,7 +388,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def "Upstream is valid if all setting are valid"() { def "Upstream is valid if all setting are valid"() {
setup: setup:
def options = ChainOptions.PartialOptions.getDefaults().buildOptions() def options = ChainOptions.PartialOptions.getDefaults().buildOptions()
def up = Mock(EthereumLikeRpcUpstream) { def up = Mock(Upstream) {
5 * getIngressReader() >> Mock(Reader) { 5 * getIngressReader() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null)) 1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null))
1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null)) 1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null))
@@ -396,8 +401,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new JsonRpcResponse('"result"'.getBytes(), null)) Mono.just(new JsonRpcResponse('"result"'.getBytes(), null))
} }
} }
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
// "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"
when: when:
def act = validator.validateUpstreamSettingsOnStartup() def act = validator.validateUpstreamSettingsOnStartup()
then: then:
@@ -407,7 +412,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def "Upstream is not valid if there are errors"() { def "Upstream is not valid if there are errors"() {
setup: setup:
def options = ChainOptions.PartialOptions.getDefaults().buildOptions() def options = ChainOptions.PartialOptions.getDefaults().buildOptions()
def up = Mock(EthereumLikeRpcUpstream) { def up = Mock(Upstream) {
5 * getIngressReader() >> Mock(Reader) { 5 * getIngressReader() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse(null, new JsonRpcError(1, "Too long"))) 1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse(null, new JsonRpcError(1, "Too long")))
1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse(null, new JsonRpcError(1, "Too long"))) 1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse(null, new JsonRpcError(1, "Too long")))
@@ -420,8 +425,8 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new JsonRpcResponse('"result"'.getBytes(), null)) Mono.just(new JsonRpcResponse('"result"'.getBytes(), null))
} }
} }
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, conf)
// "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"
when: when:
def act = validator.validateUpstreamSettingsOnStartup() def act = validator.validateUpstreamSettingsOnStartup()
then: then:

View File

@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock import io.emeraldpay.dshackle.test.GenericUpstreamMock
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.BlockValidator import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
@@ -41,7 +41,7 @@ import java.time.temporal.ChronoUnit
class EthereumWsHeadSpec extends Specification { class EthereumWsHeadSpec extends Specification {
BlockHash parent = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200") BlockHash parent = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
DefaultUpstream upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api()) DefaultUpstream upstream = new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api())
def "Fetch block"() { def "Fetch block"() {
setup: setup:

View File

@@ -1,7 +1,7 @@
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock import io.emeraldpay.dshackle.test.GenericUpstreamMock
import io.emeraldpay.dshackle.test.MockWSServer import io.emeraldpay.dshackle.test.MockWSServer
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
@@ -15,7 +15,7 @@ import spock.lang.Specification
import java.time.Duration import java.time.Duration
class WsConnectionImplRealSpec extends Specification { class WsConnectionImplRealSpec extends Specification {
DefaultUpstream upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api()) DefaultUpstream upstream = new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api())
static SLEEP = 500 static SLEEP = 500

View File

@@ -17,7 +17,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock import io.emeraldpay.dshackle.test.GenericUpstreamMock
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -32,7 +32,7 @@ import spock.lang.Specification
import java.time.Duration import java.time.Duration
class WsConnectionImplSpec extends Specification { class WsConnectionImplSpec extends Specification {
DefaultUpstream upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api()) DefaultUpstream upstream = new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api())
def "Makes a RPC call"() { def "Makes a RPC call"() {
setup: setup:

View File

@@ -20,7 +20,7 @@ import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.dshackle.upstream.generic.GenericMultistream
import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
@@ -38,7 +38,7 @@ class ConnectBlockUpdatesSpec extends Specification {
def "Extracts updates"() { def "Extracts updates"() {
setup: setup:
def connectBlockUpdates = new ConnectBlockUpdates(Stub(EthereumMultistream), Schedulers.boundedElastic()) def connectBlockUpdates = new ConnectBlockUpdates(Stub(GenericMultistream), Schedulers.boundedElastic())
def block = BlockContainer.from(new BlockJson<TransactionRefJson>().tap { def block = BlockContainer.from(new BlockJson<TransactionRefJson>().tap {
hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c") hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
number = 13412871 number = 13412871
@@ -72,7 +72,7 @@ class ConnectBlockUpdatesSpec extends Specification {
def "Produce DROP updates for replaced block"() { def "Produce DROP updates for replaced block"() {
setup: setup:
def connectBlockUpdates = new ConnectBlockUpdates(Stub(EthereumMultistream), Schedulers.boundedElastic()) def connectBlockUpdates = new ConnectBlockUpdates(Stub(GenericMultistream), Schedulers.boundedElastic())
def block = BlockContainer.from(new BlockJson<TransactionRefJson>().tap { def block = BlockContainer.from(new BlockJson<TransactionRefJson>().tap {
hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c") hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
number = 13412871 number = 13412871
@@ -106,7 +106,7 @@ class ConnectBlockUpdatesSpec extends Specification {
def "Gets prev version if available"() { def "Gets prev version if available"() {
setup: setup:
def connectBlockUpdates = new ConnectBlockUpdates(Stub(EthereumMultistream), Schedulers.boundedElastic()) def connectBlockUpdates = new ConnectBlockUpdates(Stub(GenericMultistream), Schedulers.boundedElastic())
def block1 = BlockContainer.from(new BlockJson<TransactionRefJson>().tap { def block1 = BlockContainer.from(new BlockJson<TransactionRefJson>().tap {
hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c") hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
number = 13412871 number = 13412871
@@ -170,7 +170,7 @@ class ConnectBlockUpdatesSpec extends Specification {
def "Marks old txes as dropped before producing a new version of same block"() { def "Marks old txes as dropped before producing a new version of same block"() {
setup: setup:
def connectBlockUpdates = new ConnectBlockUpdates(Stub(EthereumMultistream), Schedulers.boundedElastic()) def connectBlockUpdates = new ConnectBlockUpdates(Stub(GenericMultistream), Schedulers.boundedElastic())
def block1 = BlockContainer.from(new BlockJson<TransactionRefJson>().tap { def block1 = BlockContainer.from(new BlockJson<TransactionRefJson>().tap {
hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c") hash = BlockHash.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
number = 13412871 number = 13412871
@@ -232,7 +232,7 @@ class ConnectBlockUpdatesSpec extends Specification {
def head = Mock(Head) { def head = Mock(Head) {
1 * getFlux() >> Flux.never() 1 * getFlux() >> Flux.never()
} }
def up = Mock(EthereumMultistream) { def up = Mock(GenericMultistream) {
1 * getEnrichedHead(Selector.empty) >> head 1 * getEnrichedHead(Selector.empty) >> head
} }
def connectBlockUpdates = new ConnectBlockUpdates(up, Schedulers.boundedElastic()) def connectBlockUpdates = new ConnectBlockUpdates(up, Schedulers.boundedElastic())

View File

@@ -16,7 +16,6 @@
package io.emeraldpay.dshackle.upstream.ethereum.subscribe package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
import io.emeraldpay.etherjar.domain.Address import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.BlockHash
@@ -91,7 +90,7 @@ class ConnectLogsSpec extends Specification {
def "Filter is empty"() { def "Filter is empty"() {
setup: setup:
def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic()) def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic())
when: when:
def input = Flux.fromIterable([ def input = Flux.fromIterable([
log1, log2, log3, log4 log1, log2, log3, log4
@@ -109,7 +108,7 @@ class ConnectLogsSpec extends Specification {
def "Filter by address"() { def "Filter by address"() {
setup: setup:
def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic()) def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic())
when: when:
def input = Flux.fromIterable([ def input = Flux.fromIterable([
log1, log2 log1, log2
@@ -124,7 +123,7 @@ class ConnectLogsSpec extends Specification {
def "Filter by topic"() { def "Filter by topic"() {
setup: setup:
def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic()) def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic())
when: when:
def input = Flux.fromIterable([ def input = Flux.fromIterable([
log1, log2, log3, log4 log1, log2, log3, log4
@@ -141,7 +140,7 @@ class ConnectLogsSpec extends Specification {
def "Filter by address and topic"() { def "Filter by address and topic"() {
setup: setup:
def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Schedulers.boundedElastic()) def connectLogs = new ConnectLogs(TestingCommons.emptyMultistream(), Schedulers.boundedElastic())
when: when:
def input = Flux.fromIterable([ def input = Flux.fromIterable([
log1, log2, log3, log4 log1, log2, log3, log4

View File

@@ -3,7 +3,7 @@ package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.dshackle.upstream.generic.GenericMultistream
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import reactor.test.StepVerifier import reactor.test.StepVerifier
@@ -18,7 +18,7 @@ class ConnectNewHeadsSpec extends Specification {
TestingCommons.blockForEthereum(100) TestingCommons.blockForEthereum(100)
]) ])
} }
def up = Mock(EthereumMultistream) { def up = Mock(GenericMultistream) {
1 * getHead(Selector.empty) >> head 1 * getHead(Selector.empty) >> head
} }
ConnectNewHeads connectNewHeads = new ConnectNewHeads(up, Schedulers.boundedElastic()) ConnectNewHeads connectNewHeads = new ConnectNewHeads(up, Schedulers.boundedElastic())

View File

@@ -1,264 +0,0 @@
/**
* Copyright (c) 2019 ETCDEV GmbH
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.grpc
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.test.MockGrpcServer
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.BuildInfo
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
import io.emeraldpay.dshackle.upstream.rpcclient.RpcMetrics
import io.emeraldpay.etherjar.domain.BlockHash
import io.grpc.stub.StreamObserver
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.Timer
import reactor.core.scheduler.Schedulers
import spock.lang.Specification
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
class EthereumGrpcUpstreamSpec extends Specification {
MockGrpcServer mockServer = new MockGrpcServer()
ObjectMapper objectMapper = Global.objectMapper
RpcMetrics metrics = new RpcMetrics(
Timer.builder("test1").register(TestingCommons.meterRegistry),
Counter.builder("test2").register(TestingCommons.meterRegistry)
)
BlockHash parent = BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
def hash = (byte)123
def buildInfo = new BuildInfo("v0.0.1-test")
def "Subscribe to head"() {
setup:
def callData = [:]
def chain = Chain.ETHEREUM__MAINNET
def api = TestingCommons.api()
def block1 = new BlockJson().with {
it.number = 650246
it.hash = BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
it.totalDifficulty = new BigInteger("35bbde5595de6456", 16)
it.timestamp = Instant.now()
it.parentHash = parent
return it
}
api.answer("eth_getBlockByHash", [block1.hash.toHex(), false], block1)
def client = mockServer.clientForServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
api.nativeCall(request, responseObserver)
}
@Override
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
callData.chain = request.getTypeValue()
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId(block1.hash.toHex().substring(2))
.setHeight(block1.number)
.setParentBlockId(parent.toHex().substring(2))
.setWeight(ByteString.copyFrom(block1.totalDifficulty.toByteArray()))
.build()
)
}
})
def upstream = new EthereumGrpcUpstream("test", hash, UpstreamsConfig.UpstreamRole.PRIMARY, chain, client, new JsonRpcGrpcClient(client, chain, metrics), null, ChainsConfig.ChainConfig.default(), Schedulers.boundedElastic())
upstream.setLag(0)
upstream.update(
BlockchainOuterClass.DescribeChain.newBuilder()
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
.addAllSupportedMethods(["eth_getBlockByHash"])
.build(),
BlockchainOuterClass.BuildInfo.newBuilder()
.setVersion(buildInfo.version)
.build(),
)
when:
new Thread({ Thread.sleep(50); upstream.head.start() }).start()
def h = upstream.head.getFlux().next().block(Duration.ofSeconds(1))
then:
callData.chain == Chain.ETHEREUM__MAINNET.id
upstream.status == UpstreamAvailability.OK
upstream.getBuildInfo() == buildInfo
h.hash == BlockId.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
}
def "Follows difficulty, ignores less difficult"() {
setup:
def api = TestingCommons.api()
def block1 = new BlockJson().with {
it.number = 650246
it.hash = BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
it.totalDifficulty = new BigInteger("35bbde5595de6456", 16)
it.timestamp = Instant.now()
it.parentHash = parent
return it
}
def block2 = new BlockJson().with {
it.number = 650247
it.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec891521a")
it.totalDifficulty = new BigInteger("35bbde5595de6455", 16)
it.timestamp = Instant.now()
it.parentHash = parent
return it
}
api.answer("eth_getBlockByHash", [block1.hash.toHex(), false], block1)
api.answer("eth_getBlockByHash", [block2.hash.toHex(), false], block2)
def client = mockServer.clientForServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
api.nativeCall(request, responseObserver)
}
@Override
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
new Thread({
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId(block1.hash.toHex().substring(2))
.setHeight(block1.number)
.setParentBlockId(parent.toHex().substring(2))
.setWeight(ByteString.copyFrom(block1.totalDifficulty.toByteArray()))
.build()
)
Thread.sleep(100)
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId(block2.hash.toHex().substring(2))
.setHeight(block2.number)
.setParentBlockId(parent.toHex().substring(2))
.setWeight(ByteString.copyFrom(block2.totalDifficulty.toByteArray()))
.build()
)
}).start()
}
})
def upstream = new EthereumGrpcUpstream("test", hash, UpstreamsConfig.UpstreamRole.PRIMARY, Chain.ETHEREUM__MAINNET, client, new JsonRpcGrpcClient(client, Chain.ETHEREUM__MAINNET, metrics), null, ChainsConfig.ChainConfig.default(), Schedulers.boundedElastic())
upstream.setLag(0)
upstream.update(
BlockchainOuterClass.DescribeChain.newBuilder()
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
.addAllSupportedMethods(["eth_getBlockByHash"])
.build(),
BlockchainOuterClass.BuildInfo.newBuilder()
.setVersion(buildInfo.version)
.build(),
)
when:
new Thread({ Thread.sleep(50); upstream.head.start() }).start()
def h = upstream.head.getFlux().take(Duration.ofSeconds(1)).last().block(Duration.ofSeconds(2))
then:
upstream.status == UpstreamAvailability.OK
upstream.getBuildInfo() == buildInfo
h.hash == BlockId.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
h.height == 650246
}
def "Follows difficulty"() {
setup:
def callData = [:]
def finished = new CompletableFuture<Boolean>()
def chain = Chain.ETHEREUM__MAINNET
def api = TestingCommons.api()
def block1 = new BlockJson().with {
it.number = 650246
it.hash = BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
it.totalDifficulty = new BigInteger("35bbde5595de6456", 16)
it.timestamp = Instant.now()
it.parentHash = parent
return it
}
def block2 = new BlockJson().with {
it.number = 650247
it.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec891521a")
it.totalDifficulty = new BigInteger("35bbde5595de6457", 16)
it.timestamp = Instant.now()
it.parentHash = parent
return it
}
api.answer("eth_getBlockByHash", [block1.hash.toHex(), false], block1)
api.answer("eth_getBlockByHash", [block2.hash.toHex(), false], block2)
def client = mockServer.clientForServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
api.nativeCall(request, responseObserver)
}
@Override
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId(block1.hash.toHex().substring(2))
.setHeight(block1.number)
.setParentBlockId(parent.toHex().substring(2))
.setWeight(ByteString.copyFrom(block1.totalDifficulty.toByteArray()))
.build()
)
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId(block2.hash.toHex().substring(2))
.setHeight(block2.number)
.setParentBlockId(parent.toHex().substring(2))
.setWeight(ByteString.copyFrom(block2.totalDifficulty.toByteArray()))
.build()
)
finished.complete(true)
}
})
def upstream = new EthereumGrpcUpstream("test", hash, UpstreamsConfig.UpstreamRole.PRIMARY, chain, client, new JsonRpcGrpcClient(client, chain, metrics), null, ChainsConfig.ChainConfig.default(), Schedulers.boundedElastic())
upstream.setLag(0)
upstream.update(
BlockchainOuterClass.DescribeChain.newBuilder()
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
.addAllSupportedMethods(["eth_getBlockByHash"])
.build(),
BlockchainOuterClass.BuildInfo.newBuilder()
.setVersion(buildInfo.version)
.build(),
)
when:
new Thread({ Thread.sleep(50); upstream.head.start() }).start()
finished.get()
def h = upstream.head.getFlux().take(Duration.ofSeconds(1)).last().block(Duration.ofSeconds(2))
then:
upstream.status == UpstreamAvailability.OK
upstream.getBuildInfo() == buildInfo
h.hash == BlockId.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec891521a")
h.height == 650247
}
private BlockchainOuterClass.DescribeChain describe(List<String> methods) {
return BlockchainOuterClass.DescribeChain.newBuilder()
.setStatus(BlockchainOuterClass.ChainStatus.newBuilder().setQuorum(1).setAvailabilityValue(UpstreamAvailability.OK.grpcId))
.addAllSupportedMethods(methods)
.build()
}
}