diff --git a/docs/reference-configuration.adoc b/docs/reference-configuration.adoc index 55f4d696..0eb6a451 100644 --- a/docs/reference-configuration.adoc +++ b/docs/reference-configuration.adoc @@ -744,6 +744,11 @@ See link:09-quorum-and-selectors.adoc[Quorum and Selectors] | `false` | Disables all the validations of the upstream. I.e., it turns off `validate-peers` and `validate-syncing` checks if set to `true`. +| `validate-chain` +| boolean +| `true` +| Disables validation of the chain settings of the upstream. Prevent of creating an upstream with incorrect chain if it relates to a node with another chain. + | `validation-interval` | number | `30` diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index f64aeb99..448eb606 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -37,7 +37,8 @@ open class UpstreamsConfig { val validatePeers: Boolean, val minPeers: Int, val validateSyncing: Boolean, - val validateCallLimit: Boolean + val validateCallLimit: Boolean, + val validateChain: Boolean ) open class PartialOptions { @@ -61,6 +62,7 @@ open class UpstreamsConfig { field = value } var validateSyncing: Boolean? = null + var validateChain: Boolean? = null fun merge(overwrites: PartialOptions?): PartialOptions { if (overwrites == null) { @@ -75,6 +77,7 @@ open class UpstreamsConfig { copy.validateSyncing = firstNonNull(overwrites.validateSyncing, this.validateSyncing) copy.validateCalllimit = firstNonNull(overwrites.validateCalllimit, this.validateCalllimit) copy.timeout = firstNonNull(overwrites.timeout, this.timeout) + copy.validateChain = firstNonNull(overwrites.validateChain, this.validateChain) return copy } @@ -87,7 +90,8 @@ open class UpstreamsConfig { firstNonNull(this.validatePeers, true)!!, firstNonNull(this.minPeers, 1)!!, firstNonNull(this.validateSyncing, true)!!, - firstNonNull(this.validateCalllimit, true)!! + firstNonNull(this.validateCalllimit, true)!!, + firstNonNull(this.validateChain, true)!! ) companion object { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt index b222d563..03d69a9f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt @@ -356,6 +356,9 @@ class UpstreamsConfigReader( getValueAsBool(values, "validate-call-limit")?.let { options.validateCalllimit = it } + getValueAsBool(values, "validate-chain")?.let { + options.validateChain = it + } getValueAsInt(values, "min-peers")?.let { options.minPeers = it } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index f8c0271a..74bafc1e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -98,7 +98,7 @@ open class ConfiguredUpstreams( override fun run(args: ApplicationArguments) { log.debug("Starting upstreams") val defaultOptions = buildDefaultOptions(config) - config.upstreams.forEach { up -> + config.upstreams.parallelStream().forEach { up -> if (!up.isEnabled) { log.debug("Upstream ${up.id} is disabled") return@forEach @@ -239,6 +239,7 @@ open class ConfiguredUpstreams( true ) upstream.start() + if (!upstream.isRunning) return null return upstream } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt index 4b0f5fbb..20b5f02f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt @@ -69,6 +69,44 @@ class DefaultEthereumMethods( "debug_traceCallMany", "debug_traceTransaction" ) + + val CHAIN_DATA = mapOf( + Chain.ETHEREUM__MAINNET to HardcodedData("\"1\"", "\"0x1\""), + Chain.ETHEREUM__RINKEBY to HardcodedData("\"4\"", "\"0x4\""), + Chain.ETHEREUM__ROPSTEN to HardcodedData("\"3\"", "\"0x3\""), + Chain.ETHEREUM__KOVAN to HardcodedData("\"42\"", "\"0x2a\""), + Chain.ETHEREUM__GOERLI to HardcodedData("\"5\"", "\"0x5\""), + Chain.ETHEREUM__SEPOLIA to HardcodedData("\"11155111\"", "\"0xaa36a7\""), + + Chain.ETHEREUM_CLASSIC__MAINNET to HardcodedData("\"1\"", "\"0x3d\""), + + Chain.POLYGON_POS__MAINNET to HardcodedData("\"137\"", "\"0x89\""), + Chain.POLYGON_POS__MUMBAI to HardcodedData("\"80001\"", "\"0x13881\""), + + Chain.ARBITRUM__MAINNET to HardcodedData("\"42161\"", "\"0xa4b1\""), + Chain.ARBITRUM__GOERLI to HardcodedData("\"421613\"", "\"0x66eed\""), + + Chain.OPTIMISM__MAINNET to HardcodedData("\"10\"", "\"0xa\""), + Chain.OPTIMISM__GOERLI to HardcodedData("\"420\"", "\"0x1A4\""), + + Chain.ARBITRUM_NOVA__MAINNET to HardcodedData("\"42170\"", "\"0xa4ba\""), + + Chain.POLYGON_ZKEVM__MAINNET to HardcodedData("\"1101\"", "\"0x44d\""), + Chain.POLYGON_ZKEVM__TESTNET to HardcodedData("\"1442\"", "\"0x5a2\""), + + Chain.ZKSYNC__MAINNET to HardcodedData("\"324\"", "\"0x144\""), + Chain.ZKSYNC__TESTNET to HardcodedData("\"280\"", "\"0x118\""), + + Chain.BSC__MAINNET to HardcodedData("\"56\"", "\"0x38\""), + + Chain.BASE__MAINNET to HardcodedData("\"8453\"", "\"0x2105\""), + Chain.BASE__GOERLI to HardcodedData("\"84531\"", "\"0x14a33\""), + + Chain.LINEA__MAINNET to HardcodedData("\"59144\"", "\"0xe708\""), + Chain.LINEA__GOERLI to HardcodedData("\"59140\"", "\"0xe704\""), + ) + + fun getChainByData(data: HardcodedData) = CHAIN_DATA.entries.find { it.value == data }?.key } private val anyResponseMethods = listOf( @@ -243,48 +281,12 @@ class DefaultEthereumMethods( data class HardcodedData(val netVersion: String, val chainId: String) - private val hardcodedData = mapOf( - Chain.ETHEREUM__MAINNET to HardcodedData("\"1\"", "\"0x1\""), - Chain.ETHEREUM__RINKEBY to HardcodedData("\"4\"", "\"0x4\""), - Chain.ETHEREUM__ROPSTEN to HardcodedData("\"3\"", "\"0x3\""), - Chain.ETHEREUM__KOVAN to HardcodedData("\"42\"", "\"0x2a\""), - Chain.ETHEREUM__GOERLI to HardcodedData("\"5\"", "\"0x5\""), - Chain.ETHEREUM__SEPOLIA to HardcodedData("\"11155111\"", "\"0xaa36a7\""), - - Chain.ETHEREUM_CLASSIC__MAINNET to HardcodedData("\"1\"", "\"0x3d\""), - - Chain.POLYGON_POS__MAINNET to HardcodedData("\"137\"", "\"0x89\""), - Chain.POLYGON_POS__MUMBAI to HardcodedData("\"80001\"", "\"0x13881\""), - - Chain.ARBITRUM__MAINNET to HardcodedData("\"42161\"", "\"0xa4b1\""), - Chain.ARBITRUM__GOERLI to HardcodedData("\"421613\"", "\"0x66eed\""), - - Chain.OPTIMISM__MAINNET to HardcodedData("\"10\"", "\"0xa\""), - Chain.OPTIMISM__GOERLI to HardcodedData("\"420\"", "\"0x1A4\""), - - Chain.ARBITRUM_NOVA__MAINNET to HardcodedData("\"42170\"", "\"0xa4ba\""), - - Chain.POLYGON_ZKEVM__MAINNET to HardcodedData("\"1101\"", "\"0x44d\""), - Chain.POLYGON_ZKEVM__TESTNET to HardcodedData("\"1442\"", "\"0x5a2\""), - - Chain.ZKSYNC__MAINNET to HardcodedData("\"324\"", "\"0x144\""), - Chain.ZKSYNC__TESTNET to HardcodedData("\"280\"", "\"0x118\""), - - Chain.BSC__MAINNET to HardcodedData("\"56\"", "\"0x38\""), - - Chain.BASE__MAINNET to HardcodedData("\"8453\"", "\"0x2105\""), - Chain.BASE__GOERLI to HardcodedData("\"84531\"", "\"0x14a33\""), - - Chain.LINEA__MAINNET to HardcodedData("\"59144\"", "\"0xe708\""), - Chain.LINEA__GOERLI to HardcodedData("\"59140\"", "\"0xe704\""), - ) - override fun executeHardcoded(method: String): ByteArray { // note that the value is in json representation, i.e. if it's a string it should be with quotes, // that's why "\"0x0\"", "\"1\"", etc. But just "true" for a boolean, or "[]" for array. val json = when (method) { - "net_version" -> hardcodedData.get(chain)?.netVersion ?: throw RpcException(-32602, "Invalid chain") - "eth_chainId" -> hardcodedData.get(chain)?.chainId ?: throw RpcException(-32602, "Invalid chain") + "net_version" -> CHAIN_DATA.get(chain)?.netVersion ?: throw RpcException(-32602, "Invalid chain") + "eth_chainId" -> CHAIN_DATA.get(chain)?.chainId ?: throw RpcException(-32602, "Invalid chain") "net_peerCount" -> { "\"0x2a\"" diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt index 41830f25..6d08e335 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLikeRpcUpstream.kt @@ -45,7 +45,7 @@ open class EthereumLikeRpcUpstream( chainConfig: ChainsConfig.ChainConfig, skipEnhance: Boolean ) : EthereumLikeUpstream(id, hash, options, role, targets, node, chainConfig), Lifecycle, Upstream, CachesEnabled { - private val validator: EthereumUpstreamValidator = EthereumUpstreamValidator(this, getOptions(), chainConfig.callLimitContract) + private val validator: EthereumUpstreamValidator = EthereumUpstreamValidator(chain, this, getOptions(), chainConfig.callLimitContract) private val connector: EthereumConnector = connectorFactory.create(this, validator, chain, skipEnhance) private val labelsDetector = EthereumLabelsDetector(this.getIngressReader()) @@ -60,6 +60,11 @@ open class EthereumLikeRpcUpstream( override fun start() { log.info("Configured for ${chain.chainName}") connector.start() + if (!validator.validateUpstreamSettings()) { + connector.stop() + log.warn("Upstream ${getId()} couldn't start, invalid upstream settings") + return + } if (getOptions().disableValidation) { log.warn("Disable validation for upstream ${this.getId()}") this.setLag(0) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt index fafd02e8..997cda8f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt @@ -17,11 +17,15 @@ package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods +import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods.Companion.CHAIN_DATA +import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods.Companion.getChainByData import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.etherjar.domain.Address @@ -33,17 +37,17 @@ import org.springframework.scheduling.concurrent.CustomizableThreadFactory import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers -import reactor.util.function.Tuple3 +import reactor.util.function.Tuple2 import java.time.Duration import java.util.concurrent.Executors import java.util.concurrent.TimeoutException open class EthereumUpstreamValidator @JvmOverloads constructor( + private val chain: Chain, private val upstream: Upstream, private val options: UpstreamsConfig.Options, private val callLimitContract: String? = null ) { - private var callLimitSucceed: Boolean = false companion object { private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java) val scheduler = @@ -55,55 +59,16 @@ open class EthereumUpstreamValidator @JvmOverloads constructor( open fun validate(): Mono { return Mono.zip( validateSyncing(), - validatePeers(), - validateCallLimit() + validatePeers() ) .map(::resolve) .defaultIfEmpty(UpstreamAvailability.UNAVAILABLE) .onErrorReturn(UpstreamAvailability.UNAVAILABLE) } - fun resolve(results: Tuple3): UpstreamAvailability { + fun resolve(results: Tuple2): UpstreamAvailability { val cp = Comparator { avail1: UpstreamAvailability, avail2: UpstreamAvailability -> if (avail1.isBetterTo(avail2)) -1 else 1 } - return listOf(results.t1, results.t2, results.t3).sortedWith(cp).last() - } - - fun validateCallLimit(): Mono { - // do not rerun this check after first success because it's more expensive than others - if (!options.validateCallLimit || callLimitContract == null || callLimitSucceed) { - return Mono.just(UpstreamAvailability.OK) - } - return upstream.getIngressReader() - .read( - JsonRpcRequest( - "eth_call", - listOf( - TransactionCallJson( - Address.from(callLimitContract), - // calling contract with param 200_000, meaning it will generate 200k symbols or response - // 30ce0 + metadata — 200_000k - HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") - ), - "latest" - ) - ) - ) - .flatMap(JsonRpcResponse::requireResult) - .doOnError { - log.error( - "Error: ${it.message}. Node ${upstream.getId()} is prbably incorrectly configured. " + - "You need to set up your return limit to at least 200000. " + - "Erigon config example: https://github.com/ledgerwatch/erigon/blob/devel/cmd/utils/flags.go#L364. " - ) - } - .map { UpstreamAvailability.OK } - .timeout( - Defaults.timeoutInternal, - Mono.fromCallable { log.error("No response for eth_call limit check from ${upstream.getId()}") } - .then(Mono.error(TimeoutException("Validation timeout for call limit"))) - ) - .doOnSuccess { callLimitSucceed = true } - .onErrorReturn(UpstreamAvailability.UNAVAILABLE) + return listOf(results.t1, results.t2).sortedWith(cp).last() } fun validateSyncing(): Mono { @@ -165,4 +130,95 @@ open class EthereumUpstreamValidator @JvmOverloads constructor( validate() } } + + fun validateUpstreamSettings(): Boolean { + return Mono.zip( + validateChain(), + validateCallLimit() + ).map { + it.t1 && it.t2 + }.block() ?: false + } + + private fun validateChain(): Mono { + if (!options.validateChain) { + return Mono.just(true) + } + return Mono.zip( + chainId(), + netVersion() + ) + .map { + val chainData = CHAIN_DATA[chain] ?: return@map false + val isChainValid = chainData.chainId == it.t1 && chainData.netVersion == it.t2 + + if (!isChainValid) { + val actualChain = getChainByData( + DefaultEthereumMethods.HardcodedData(it.t2, it.t1) + )?.chainName + log.warn( + "${chain.chainName} is specified for upstream ${upstream.getId()} " + + "but actually it is $actualChain with chainId ${it.t1} and net_version ${it.t2}" + ) + } + + isChainValid + } + .onErrorResume { + log.error("Error during chain validation", it) + Mono.just(false) + } + } + + private fun validateCallLimit(): Mono { + if (!options.validateCallLimit || callLimitContract == null) { + return Mono.just(true) + } + return upstream.getIngressReader() + .read( + JsonRpcRequest( + "eth_call", + listOf( + TransactionCallJson( + Address.from(callLimitContract), + // calling contract with param 200_000, meaning it will generate 200k symbols or response + // 30ce0 + metadata — 200_000k + HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") + ), + "latest" + ) + ) + ) + .flatMap(JsonRpcResponse::requireResult) + .doOnError { + log.warn( + "Error: ${it.message}. Node ${upstream.getId()} is probably incorrectly configured. " + + "You need to set up your return limit to at least 200000. " + + "Erigon config example: https://github.com/ledgerwatch/erigon/blob/devel/cmd/utils/flags.go#L364. " + ) + } + .map { true } + .timeout( + Defaults.timeoutInternal, + Mono.fromCallable { log.error("No response for eth_call limit check from ${upstream.getId()}") } + .then(Mono.error(TimeoutException("Validation timeout for call limit"))) + ) + .onErrorReturn(false) + } + + private fun chainId(): Mono { + return upstream.getIngressReader() + .read(JsonRpcRequest("eth_chainId", emptyList())) + .doOnError { log.error("Error during execution 'eth_chainId' - ${it.message}") } + .flatMap(JsonRpcResponse::requireResult) + .map { String(it) } + } + + private fun netVersion(): Mono { + return upstream.getIngressReader() + .read(JsonRpcRequest("net_version", emptyList())) + .doOnError { log.error("Error during execution 'net_version' - ${it.message}") } + .flatMap(JsonRpcResponse::requireResult) + .map { String(it) } + } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy index 4527a57f..5308f510 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy @@ -635,7 +635,7 @@ class UpstreamsConfigReaderSpec extends Specification { def options = partialOptions.buildOptions() then: options == new UpstreamsConfig.Options( - false, 30, Duration.ofSeconds(60), null, true, 1, true, true + false, 30, Duration.ofSeconds(60), null, true, 1, true, true, true ) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/HeadLagObserverSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/HeadLagObserverSpec.groovy index 9c156a67..8950135f 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/HeadLagObserverSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/HeadLagObserverSpec.groovy @@ -117,7 +117,7 @@ class HeadLagObserverSpec extends Specification { TestHeadLagObserver(@NotNull Head master, @NotNull Collection followers) { super(master, followers, DistanceExtractor.@Companion::extractPowDistance, - Schedulers.parallel(), Duration.ofNanos(1)) + Schedulers.boundedElastic(), Duration.ofNanos(1)) } @Override diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy index 9fdea0ae..d437790b 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy @@ -15,30 +15,35 @@ */ package io.emeraldpay.dshackle.upstream.ethereum -import io.emeraldpay.dshackle.config.ChainsConfig + import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.test.ApiReaderMock import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.emeraldpay.etherjar.domain.Address import io.emeraldpay.etherjar.hex.HexData import io.emeraldpay.etherjar.rpc.RpcResponseError import io.emeraldpay.etherjar.rpc.json.TransactionCallJson -import io.emeraldpay.etherjar.domain.Address import reactor.core.publisher.Mono import reactor.util.function.Tuples import spock.lang.Specification import java.time.Duration +import static io.emeraldpay.dshackle.Chain.ETHEREUM__MAINNET +import static io.emeraldpay.dshackle.Chain.OPTIMISM__MAINNET import static io.emeraldpay.dshackle.upstream.UpstreamAvailability.* +import static java.util.Collections.emptyList class EthereumUpstreamValidatorSpec extends Specification { def "Resolve to final availability"() { setup: - def validator = new EthereumUpstreamValidator(Stub(EthereumLikeUpstream), UpstreamsConfig.PartialOptions.getDefaults().buildOptions()) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, Stub(EthereumLikeUpstream), UpstreamsConfig.PartialOptions.getDefaults().buildOptions()) expect: validator.resolve(Tuples.of(sync, peers, call)) == exp where: @@ -60,7 +65,7 @@ class EthereumUpstreamValidatorSpec extends Specification { it.validateSyncing = false }.buildOptions() def up = Mock(EthereumLikeUpstream) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validateSyncing().block(Duration.ofSeconds(1)) @@ -79,7 +84,7 @@ class EthereumUpstreamValidatorSpec extends Specification { answer("eth_syncing", [], false) } ) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validateSyncing().block(Duration.ofSeconds(1)) @@ -104,7 +109,7 @@ class EthereumUpstreamValidatorSpec extends Specification { 1 * head.onSyncingNode(false) } } - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validateSyncing().block(Duration.ofSeconds(1)) @@ -124,7 +129,7 @@ class EthereumUpstreamValidatorSpec extends Specification { answer("eth_syncing", [], [startingBlock: 100, currentBlock: 50]) } ) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validateSyncing().block(Duration.ofSeconds(1)) @@ -142,7 +147,7 @@ class EthereumUpstreamValidatorSpec extends Specification { answer("eth_syncing", [], new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unavailable")) } ) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validateSyncing().block(Duration.ofSeconds(1)) @@ -157,7 +162,7 @@ class EthereumUpstreamValidatorSpec extends Specification { it.minPeers = 10 }.buildOptions() def up = Mock(EthereumLikeUpstream) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validatePeers().block(Duration.ofSeconds(1)) @@ -173,7 +178,7 @@ class EthereumUpstreamValidatorSpec extends Specification { it.minPeers = 0 }.buildOptions() def up = Mock(EthereumLikeUpstream) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validatePeers().block(Duration.ofSeconds(1)) @@ -193,7 +198,7 @@ class EthereumUpstreamValidatorSpec extends Specification { answer("net_peerCount", [], "0x5") } ) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validatePeers().block(Duration.ofSeconds(1)) @@ -212,7 +217,7 @@ class EthereumUpstreamValidatorSpec extends Specification { answer("net_peerCount", [], "0xa") } ) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validatePeers().block(Duration.ofSeconds(1)) @@ -231,7 +236,7 @@ class EthereumUpstreamValidatorSpec extends Specification { answer("net_peerCount", [], "0xff") } ) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validatePeers().block(Duration.ofSeconds(1)) @@ -250,7 +255,7 @@ class EthereumUpstreamValidatorSpec extends Specification { answer("net_peerCount", [], new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unavailable")) } ) - def validator = new EthereumUpstreamValidator(up, options) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) when: def act = validator.validatePeers().block(Duration.ofSeconds(1)) @@ -258,65 +263,142 @@ class EthereumUpstreamValidatorSpec extends Specification { act == UNAVAILABLE } - def "Doesnt check call limit when disabled"() { + def "Doesnt validate settings when disabled"() { + setup: + def options = UpstreamsConfig.PartialOptions.getDefaults().tap { + it.validateCalllimit = false + it.validateChain = false + }.buildOptions() + def up = Mock(EthereumLikeUpstream) + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options) + + when: + def act = validator.validateUpstreamSettings() + then: + act + 0 * up.getIngressReader() + } + + def "Upstream is valid if not error from call limit check"() { + setup: + def options = UpstreamsConfig.PartialOptions.getDefaults().tap { + it.validateChain = false + }.buildOptions() + def up = Mock(EthereumLikeRpcUpstream) { + 1 * getIngressReader() >> Mock(Reader) { + 1 * read(new JsonRpcRequest("eth_call", [new TransactionCallJson( + Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), + HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") + ), "latest"])) >> Mono.just(new JsonRpcResponse("0x00000000000000000000".getBytes(), null)) + } + } + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") + + when: + def act = validator.validateUpstreamSettings() + then: + act + } + + def "Upstream is not valid if error returned on call limit check"() { + setup: + def options = UpstreamsConfig.PartialOptions.getDefaults().tap { + it.validateChain = false + }.buildOptions() + def up = Mock(EthereumLikeRpcUpstream) { + 1 * getIngressReader() >> Mock(Reader) { + 1 * read(new JsonRpcRequest("eth_call", [new TransactionCallJson( + Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), + HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") + ), "latest"])) >> Mono.just(new JsonRpcResponse(null, new JsonRpcError(1, "Too long"))) + } + } + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") + + when: + def act = validator.validateUpstreamSettings() + then: + !act + } + + def "Upstream is valid if chain settings are valid"() { setup: def options = UpstreamsConfig.PartialOptions.getDefaults().tap { it.validateCalllimit = false }.buildOptions() - def up = Mock(EthereumLikeUpstream) - def validator = new EthereumUpstreamValidator(up, options) + def up = Mock(EthereumLikeRpcUpstream) { + 2 * getIngressReader() >> Mock(Reader) { + 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)) + } + } + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") when: - def act = validator.validateCallLimit().block(Duration.ofSeconds(1)) + def act = validator.validateUpstreamSettings() then: - act == OK - 0 * up.getIngressReader() + act } - def "Upstream available if not error from call limit check"() { + def "Upstream is not valid - specified optimism but got ethereum"() { setup: - def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions() - def up = TestingCommons.upstream( - new ApiReaderMock().tap { - answerOnce("eth_call", [new TransactionCallJson( - Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), - HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") - ), "latest"], "0x00000000000000000000") - } - ) - def validator = new EthereumUpstreamValidator(up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") + def options = UpstreamsConfig.PartialOptions.getDefaults().tap { + it.validateCalllimit = false + }.buildOptions() + def up = Mock(EthereumLikeRpcUpstream) { + 2 * getIngressReader() >> Mock(Reader) { + 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)) + } + } + def validator = new EthereumUpstreamValidator(OPTIMISM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") when: - def act = validator.validateCallLimit().block(Duration.ofSeconds(1)) + def act = validator.validateUpstreamSettings() then: - act == OK - when: - def act2 = validator.validateCallLimit().block(Duration.ofSeconds(1)) - then: - act2 == OK + !act } - def "Upstream not available if error returned on call limit check"() { + def "Upstream is valid if all setting are valid"() { setup: def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions() - def up = TestingCommons.upstream( - new ApiReaderMock().tap { - answer("eth_call", [new TransactionCallJson( - Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), - HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") - ), "latest"], new RpcResponseError(RpcResponseError.CODE_INVALID_REQUEST, "Too long")) - } - ) - def validator = new EthereumUpstreamValidator(up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") + def up = Mock(EthereumLikeRpcUpstream) { + 3 * getIngressReader() >> Mock(Reader) { + 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("eth_call", [new TransactionCallJson( + Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), + HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") + ), "latest"])) >> Mono.just(new JsonRpcResponse("0x00000000000000000000".getBytes(), null)) + } + } + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") when: - def act = validator.validateCallLimit().block(Duration.ofSeconds(1)) + def act = validator.validateUpstreamSettings() then: - act == UNAVAILABLE + act + } + + def "Upstream is not valid if there are errors"() { + setup: + def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions() + def up = Mock(EthereumLikeRpcUpstream) { + 3 * getIngressReader() >> Mock(Reader) { + 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("eth_call", [new TransactionCallJson( + Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), + HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") + ), "latest"])) >> Mono.just(new JsonRpcResponse(null, new JsonRpcError(1, "Too long"))) + } + } + def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") + when: - def act2 = validator.validateCallLimit().block(Duration.ofSeconds(1)) + def act = validator.validateUpstreamSettings() then: - act2 == UNAVAILABLE + !act } } diff --git a/src/test/kotlin/io/emeraldpay/dshackle/IntegrationTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/IntegrationTest.kt index fac73601..01350b17 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/IntegrationTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/IntegrationTest.kt @@ -95,6 +95,10 @@ class IntegrationTest { id = "ganache" nodeId = 1 chain = "ethereum" + options = UpstreamsConfig.PartialOptions() + .apply { + validateChain = false + } connection = UpstreamsConfig.EthereumPosConnection().apply { execution = UpstreamsConfig.EthereumConnection().apply { rpc = UpstreamsConfig.HttpEndpoint(