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`
| 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`

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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\""

View File

@@ -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)

View File

@@ -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<UpstreamAvailability> {
return Mono.zip(
validateSyncing(),
validatePeers(),
validateCallLimit()
validatePeers()
)
.map(::resolve)
.defaultIfEmpty(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 }
return listOf(results.t1, results.t2, results.t3).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)
return listOf(results.t1, results.t2).sortedWith(cp).last()
}
fun validateSyncing(): Mono<UpstreamAvailability> {
@@ -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<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()
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
)
}
}

View File

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

View File

@@ -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
}
}

View File

@@ -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(