Add validation of chain setting (#258)

This commit is contained in:
KirillPamPam
2023-08-01 14:02:31 +04:00
committed by GitHub
parent b0af3ab3b3
commit 9757dfd201
11 changed files with 301 additions and 139 deletions

View File

@@ -744,6 +744,11 @@ See link:09-quorum-and-selectors.adoc[Quorum and Selectors]
| `false` | `false`
| Disables all the validations of the upstream. I.e., it turns off `validate-peers` and `validate-syncing` checks if set to `true`. | 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` | `validation-interval`
| number | number
| `30` | `30`

View File

@@ -37,7 +37,8 @@ open class UpstreamsConfig {
val validatePeers: Boolean, val validatePeers: Boolean,
val minPeers: Int, val minPeers: Int,
val validateSyncing: Boolean, val validateSyncing: Boolean,
val validateCallLimit: Boolean val validateCallLimit: Boolean,
val validateChain: Boolean
) )
open class PartialOptions { open class PartialOptions {
@@ -61,6 +62,7 @@ open class UpstreamsConfig {
field = value field = value
} }
var validateSyncing: Boolean? = null var validateSyncing: Boolean? = null
var validateChain: Boolean? = null
fun merge(overwrites: PartialOptions?): PartialOptions { fun merge(overwrites: PartialOptions?): PartialOptions {
if (overwrites == null) { if (overwrites == null) {
@@ -75,6 +77,7 @@ open class UpstreamsConfig {
copy.validateSyncing = firstNonNull(overwrites.validateSyncing, this.validateSyncing) copy.validateSyncing = firstNonNull(overwrites.validateSyncing, this.validateSyncing)
copy.validateCalllimit = firstNonNull(overwrites.validateCalllimit, this.validateCalllimit) copy.validateCalllimit = firstNonNull(overwrites.validateCalllimit, this.validateCalllimit)
copy.timeout = firstNonNull(overwrites.timeout, this.timeout) copy.timeout = firstNonNull(overwrites.timeout, this.timeout)
copy.validateChain = firstNonNull(overwrites.validateChain, this.validateChain)
return copy return copy
} }
@@ -87,7 +90,8 @@ open class UpstreamsConfig {
firstNonNull(this.validatePeers, true)!!, firstNonNull(this.validatePeers, true)!!,
firstNonNull(this.minPeers, 1)!!, firstNonNull(this.minPeers, 1)!!,
firstNonNull(this.validateSyncing, true)!!, firstNonNull(this.validateSyncing, true)!!,
firstNonNull(this.validateCalllimit, true)!! firstNonNull(this.validateCalllimit, true)!!,
firstNonNull(this.validateChain, true)!!
) )
companion object { companion object {

View File

@@ -356,6 +356,9 @@ class UpstreamsConfigReader(
getValueAsBool(values, "validate-call-limit")?.let { getValueAsBool(values, "validate-call-limit")?.let {
options.validateCalllimit = it options.validateCalllimit = it
} }
getValueAsBool(values, "validate-chain")?.let {
options.validateChain = it
}
getValueAsInt(values, "min-peers")?.let { getValueAsInt(values, "min-peers")?.let {
options.minPeers = it options.minPeers = it
} }

View File

@@ -98,7 +98,7 @@ open class ConfiguredUpstreams(
override fun run(args: ApplicationArguments) { override fun run(args: ApplicationArguments) {
log.debug("Starting upstreams") log.debug("Starting upstreams")
val defaultOptions = buildDefaultOptions(config) val defaultOptions = buildDefaultOptions(config)
config.upstreams.forEach { up -> config.upstreams.parallelStream().forEach { up ->
if (!up.isEnabled) { if (!up.isEnabled) {
log.debug("Upstream ${up.id} is disabled") log.debug("Upstream ${up.id} is disabled")
return@forEach return@forEach
@@ -239,6 +239,7 @@ open class ConfiguredUpstreams(
true true
) )
upstream.start() upstream.start()
if (!upstream.isRunning) return null
return upstream return upstream
} }

View File

@@ -69,6 +69,44 @@ class DefaultEthereumMethods(
"debug_traceCallMany", "debug_traceCallMany",
"debug_traceTransaction" "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( private val anyResponseMethods = listOf(
@@ -243,48 +281,12 @@ class DefaultEthereumMethods(
data class HardcodedData(val netVersion: String, val chainId: String) 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 { 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, // 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. // that's why "\"0x0\"", "\"1\"", etc. But just "true" for a boolean, or "[]" for array.
val json = when (method) { val json = when (method) {
"net_version" -> hardcodedData.get(chain)?.netVersion ?: throw RpcException(-32602, "Invalid chain") "net_version" -> CHAIN_DATA.get(chain)?.netVersion ?: throw RpcException(-32602, "Invalid chain")
"eth_chainId" -> hardcodedData.get(chain)?.chainId ?: throw RpcException(-32602, "Invalid chain") "eth_chainId" -> CHAIN_DATA.get(chain)?.chainId ?: throw RpcException(-32602, "Invalid chain")
"net_peerCount" -> { "net_peerCount" -> {
"\"0x2a\"" "\"0x2a\""

View File

@@ -45,7 +45,7 @@ open class EthereumLikeRpcUpstream(
chainConfig: ChainsConfig.ChainConfig, chainConfig: ChainsConfig.ChainConfig,
skipEnhance: Boolean skipEnhance: Boolean
) : EthereumLikeUpstream(id, hash, options, role, targets, node, chainConfig), Lifecycle, Upstream, CachesEnabled { ) : 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 connector: EthereumConnector = connectorFactory.create(this, validator, chain, skipEnhance)
private val labelsDetector = EthereumLabelsDetector(this.getIngressReader()) private val labelsDetector = EthereumLabelsDetector(this.getIngressReader())
@@ -60,6 +60,11 @@ open class EthereumLikeRpcUpstream(
override fun start() { override fun start() {
log.info("Configured for ${chain.chainName}") log.info("Configured for ${chain.chainName}")
connector.start() connector.start()
if (!validator.validateUpstreamSettings()) {
connector.stop()
log.warn("Upstream ${getId()} couldn't start, invalid upstream settings")
return
}
if (getOptions().disableValidation) { if (getOptions().disableValidation) {
log.warn("Disable validation for upstream ${this.getId()}") log.warn("Disable validation for upstream ${this.getId()}")
this.setLag(0) this.setLag(0)

View File

@@ -17,11 +17,15 @@
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
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.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
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.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.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
@@ -33,17 +37,17 @@ import org.springframework.scheduling.concurrent.CustomizableThreadFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import reactor.util.function.Tuple3 import reactor.util.function.Tuple2
import java.time.Duration import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
open class EthereumUpstreamValidator @JvmOverloads constructor( open class EthereumUpstreamValidator @JvmOverloads constructor(
private val chain: Chain,
private val upstream: Upstream, private val upstream: Upstream,
private val options: UpstreamsConfig.Options, private val options: UpstreamsConfig.Options,
private val callLimitContract: String? = null private val callLimitContract: String? = null
) { ) {
private var callLimitSucceed: Boolean = false
companion object { companion object {
private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java) private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java)
val scheduler = val scheduler =
@@ -55,55 +59,16 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
open fun validate(): Mono<UpstreamAvailability> { open fun validate(): Mono<UpstreamAvailability> {
return Mono.zip( return Mono.zip(
validateSyncing(), validateSyncing(),
validatePeers(), validatePeers()
validateCallLimit()
) )
.map(::resolve) .map(::resolve)
.defaultIfEmpty(UpstreamAvailability.UNAVAILABLE) .defaultIfEmpty(UpstreamAvailability.UNAVAILABLE)
.onErrorReturn(UpstreamAvailability.UNAVAILABLE) .onErrorReturn(UpstreamAvailability.UNAVAILABLE)
} }
fun resolve(results: Tuple3<UpstreamAvailability, UpstreamAvailability, UpstreamAvailability>): UpstreamAvailability { fun resolve(results: Tuple2<UpstreamAvailability, UpstreamAvailability>): UpstreamAvailability {
val cp = Comparator { avail1: UpstreamAvailability, avail2: UpstreamAvailability -> if (avail1.isBetterTo(avail2)) -1 else 1 } 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() return listOf(results.t1, results.t2).sortedWith(cp).last()
}
fun validateCallLimit(): Mono<UpstreamAvailability> {
// 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)
} }
fun validateSyncing(): Mono<UpstreamAvailability> { fun validateSyncing(): Mono<UpstreamAvailability> {
@@ -165,4 +130,95 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
validate() validate()
} }
} }
fun validateUpstreamSettings(): Boolean {
return Mono.zip(
validateChain(),
validateCallLimit()
).map {
it.t1 && it.t2
}.block() ?: false
}
private fun validateChain(): Mono<Boolean> {
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<Boolean> {
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<String> {
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<String> {
return upstream.getIngressReader()
.read(JsonRpcRequest("net_version", emptyList()))
.doOnError { log.error("Error during execution 'net_version' - ${it.message}") }
.flatMap(JsonRpcResponse::requireResult)
.map { String(it) }
}
} }

View File

@@ -635,7 +635,7 @@ class UpstreamsConfigReaderSpec extends Specification {
def options = partialOptions.buildOptions() def options = partialOptions.buildOptions()
then: then:
options == new UpstreamsConfig.Options( 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
) )
} }
} }

View File

@@ -117,7 +117,7 @@ class HeadLagObserverSpec extends Specification {
TestHeadLagObserver(@NotNull Head master, @NotNull Collection<? extends Upstream> followers) { TestHeadLagObserver(@NotNull Head master, @NotNull Collection<? extends Upstream> followers) {
super(master, followers, DistanceExtractor.@Companion::extractPowDistance, super(master, followers, DistanceExtractor.@Companion::extractPowDistance,
Schedulers.parallel(), Duration.ofNanos(1)) Schedulers.boundedElastic(), Duration.ofNanos(1))
} }
@Override @Override

View File

@@ -15,30 +15,35 @@
*/ */
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.test.ApiReaderMock import io.emeraldpay.dshackle.test.ApiReaderMock
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.rpcclient.JsonRpcError
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.hex.HexData import io.emeraldpay.etherjar.hex.HexData
import io.emeraldpay.etherjar.rpc.RpcResponseError import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.emeraldpay.etherjar.rpc.json.TransactionCallJson import io.emeraldpay.etherjar.rpc.json.TransactionCallJson
import io.emeraldpay.etherjar.domain.Address
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.util.function.Tuples import reactor.util.function.Tuples
import spock.lang.Specification import spock.lang.Specification
import java.time.Duration 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 io.emeraldpay.dshackle.upstream.UpstreamAvailability.*
import static java.util.Collections.emptyList
class EthereumUpstreamValidatorSpec extends Specification { class EthereumUpstreamValidatorSpec extends Specification {
def "Resolve to final availability"() { def "Resolve to final availability"() {
setup: setup:
def validator = new EthereumUpstreamValidator(Stub(EthereumLikeUpstream), UpstreamsConfig.PartialOptions.getDefaults().buildOptions()) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, Stub(EthereumLikeUpstream), UpstreamsConfig.PartialOptions.getDefaults().buildOptions())
expect: expect:
validator.resolve(Tuples.of(sync, peers, call)) == exp validator.resolve(Tuples.of(sync, peers, call)) == exp
where: where:
@@ -60,7 +65,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
it.validateSyncing = false it.validateSyncing = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) def up = Mock(EthereumLikeUpstream)
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -79,7 +84,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("eth_syncing", [], false) answer("eth_syncing", [], false)
} }
) )
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -104,7 +109,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
1 * head.onSyncingNode(false) 1 * head.onSyncingNode(false)
} }
} }
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -124,7 +129,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("eth_syncing", [], [startingBlock: 100, currentBlock: 50]) answer("eth_syncing", [], [startingBlock: 100, currentBlock: 50])
} }
) )
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) 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")) 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: when:
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
@@ -157,7 +162,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
it.minPeers = 10 it.minPeers = 10
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) def up = Mock(EthereumLikeUpstream)
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -173,7 +178,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
it.minPeers = 0 it.minPeers = 0
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) def up = Mock(EthereumLikeUpstream)
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -193,7 +198,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("net_peerCount", [], "0x5") answer("net_peerCount", [], "0x5")
} }
) )
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -212,7 +217,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("net_peerCount", [], "0xa") answer("net_peerCount", [], "0xa")
} }
) )
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -231,7 +236,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
answer("net_peerCount", [], "0xff") answer("net_peerCount", [], "0xff")
} }
) )
def validator = new EthereumUpstreamValidator(up, options) def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options)
when: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) 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")) 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: when:
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
@@ -258,65 +263,142 @@ class EthereumUpstreamValidatorSpec extends Specification {
act == UNAVAILABLE 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: setup:
def options = UpstreamsConfig.PartialOptions.getDefaults().tap { def options = UpstreamsConfig.PartialOptions.getDefaults().tap {
it.validateCalllimit = false it.validateCalllimit = false
}.buildOptions() }.buildOptions()
def up = Mock(EthereumLikeUpstream) def up = Mock(EthereumLikeRpcUpstream) {
def validator = new EthereumUpstreamValidator(up, options) 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: when:
def act = validator.validateCallLimit().block(Duration.ofSeconds(1)) def act = validator.validateUpstreamSettings()
then: then:
act == OK act
0 * up.getIngressReader()
} }
def "Upstream available if not error from call limit check"() { def "Upstream is not valid - specified optimism but got ethereum"() {
setup: setup:
def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions() def options = UpstreamsConfig.PartialOptions.getDefaults().tap {
def up = TestingCommons.upstream( it.validateCalllimit = false
new ApiReaderMock().tap { }.buildOptions()
answerOnce("eth_call", [new TransactionCallJson( def up = Mock(EthereumLikeRpcUpstream) {
Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), 2 * getIngressReader() >> Mock(Reader) {
HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") 1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null))
), "latest"], "0x00000000000000000000") 1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null))
} }
) }
def validator = new EthereumUpstreamValidator(up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") def validator = new EthereumUpstreamValidator(OPTIMISM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96")
when: when:
def act = validator.validateCallLimit().block(Duration.ofSeconds(1)) def act = validator.validateUpstreamSettings()
then: then:
act == OK !act
when:
def act2 = validator.validateCallLimit().block(Duration.ofSeconds(1))
then:
act2 == OK
} }
def "Upstream not available if error returned on call limit check"() { def "Upstream is valid if all setting are valid"() {
setup: setup:
def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions() def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions()
def up = TestingCommons.upstream( def up = Mock(EthereumLikeRpcUpstream) {
new ApiReaderMock().tap { 3 * getIngressReader() >> Mock(Reader) {
answer("eth_call", [new TransactionCallJson( 1 * read(new JsonRpcRequest("eth_chainId", emptyList())) >> Mono.just(new JsonRpcResponse('"0x1"'.getBytes(), null))
Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"), 1 * read(new JsonRpcRequest("net_version", emptyList())) >> Mono.just(new JsonRpcResponse('"1"'.getBytes(), null))
HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0") 1 * read(new JsonRpcRequest("eth_call", [new TransactionCallJson(
), "latest"], new RpcResponseError(RpcResponseError.CODE_INVALID_REQUEST, "Too long")) Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"),
} HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030ce0")
) ), "latest"])) >> Mono.just(new JsonRpcResponse("0x00000000000000000000".getBytes(), null))
def validator = new EthereumUpstreamValidator(up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96") }
}
def validator = new EthereumUpstreamValidator(ETHEREUM__MAINNET, up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96")
when: when:
def act = validator.validateCallLimit().block(Duration.ofSeconds(1)) def act = validator.validateUpstreamSettings()
then: 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: when:
def act2 = validator.validateCallLimit().block(Duration.ofSeconds(1)) def act = validator.validateUpstreamSettings()
then: then:
act2 == UNAVAILABLE !act
} }
} }

View File

@@ -95,6 +95,10 @@ class IntegrationTest {
id = "ganache" id = "ganache"
nodeId = 1 nodeId = 1
chain = "ethereum" chain = "ethereum"
options = UpstreamsConfig.PartialOptions()
.apply {
validateChain = false
}
connection = UpstreamsConfig.EthereumPosConnection().apply { connection = UpstreamsConfig.EthereumPosConnection().apply {
execution = UpstreamsConfig.EthereumConnection().apply { execution = UpstreamsConfig.EthereumConnection().apply {
rpc = UpstreamsConfig.HttpEndpoint( rpc = UpstreamsConfig.HttpEndpoint(