Checking eth_call return limit during node validation, currently 200k (#246)

Added eth_call return limit check to node validation. Currently limit is 200k
This commit is contained in:
Vyacheslav
2023-07-12 13:43:54 +03:00
committed by GitHub
parent c29b5037a1
commit b57153826c
15 changed files with 167 additions and 32 deletions

View File

@@ -11,6 +11,7 @@ data class ChainsConfig(private val chains: Map<Chain, RawChainConfig>, val curr
data class RawChainConfig( data class RawChainConfig(
var syncingLagSize: Int? = null, var syncingLagSize: Int? = null,
var laggingLagSize: Int? = null, var laggingLagSize: Int? = null,
var callLimitContract: String? = null,
var options: UpstreamsConfig.PartialOptions? = null var options: UpstreamsConfig.PartialOptions? = null
) { ) {
@@ -26,11 +27,12 @@ data class ChainsConfig(private val chains: Map<Chain, RawChainConfig>, val curr
data class ChainConfig( data class ChainConfig(
val syncingLagSize: Int, val syncingLagSize: Int,
val laggingLagSize: Int, val laggingLagSize: Int,
val options: UpstreamsConfig.PartialOptions val options: UpstreamsConfig.PartialOptions,
val callLimitContract: String?
) { ) {
companion object { companion object {
@JvmStatic @JvmStatic
fun default() = ChainConfig(6, 1, UpstreamsConfig.PartialOptions()) fun default() = ChainConfig(6, 1, UpstreamsConfig.PartialOptions(), null)
} }
} }
@@ -42,7 +44,8 @@ data class ChainsConfig(private val chains: Map<Chain, RawChainConfig>, val curr
return ChainConfig( return ChainConfig(
laggingLagSize = raw.laggingLagSize ?: default.laggingLagSize ?: panic(), laggingLagSize = raw.laggingLagSize ?: default.laggingLagSize ?: panic(),
syncingLagSize = raw.syncingLagSize ?: default.syncingLagSize ?: panic(), syncingLagSize = raw.syncingLagSize ?: default.syncingLagSize ?: panic(),
options = options options = options,
callLimitContract = raw.callLimitContract
) )
} }
@@ -57,7 +60,8 @@ data class ChainsConfig(private val chains: Map<Chain, RawChainConfig>, val curr
) = RawChainConfig( ) = RawChainConfig(
syncingLagSize = patch?.syncingLagSize ?: current.syncingLagSize, syncingLagSize = patch?.syncingLagSize ?: current.syncingLagSize,
laggingLagSize = patch?.laggingLagSize ?: current.laggingLagSize, laggingLagSize = patch?.laggingLagSize ?: current.laggingLagSize,
options = patch?.options ?: current.options options = patch?.options ?: current.options,
callLimitContract = patch?.callLimitContract ?: current.callLimitContract
) )
private fun merge( private fun merge(

View File

@@ -53,6 +53,9 @@ class ChainsConfigReader(
rawConfig.laggingLagSize = it rawConfig.laggingLagSize = it
} }
} }
getValueAsString(node, "call-validate-contract")?.let {
rawConfig.callLimitContract = it
}
upstreamsConfigReader.tryReadOptions(node)?.let { upstreamsConfigReader.tryReadOptions(node)?.let {
rawConfig.options = it rawConfig.options = it
} }

View File

@@ -36,7 +36,8 @@ open class UpstreamsConfig {
val providesBalance: Boolean?, val providesBalance: Boolean?,
val validatePeers: Boolean, val validatePeers: Boolean,
val minPeers: Int, val minPeers: Int,
val validateSyncing: Boolean val validateSyncing: Boolean,
val validateCallLimit: Boolean
) )
open class PartialOptions { open class PartialOptions {
@@ -51,6 +52,7 @@ open class UpstreamsConfig {
var timeout: Duration? = null var timeout: Duration? = null
var providesBalance: Boolean? = null var providesBalance: Boolean? = null
var validatePeers: Boolean? = null var validatePeers: Boolean? = null
var validateCalllimit: Boolean? = null
var minPeers: Int? = null var minPeers: Int? = null
set(value) { set(value) {
require(value == null || value >= 0) { require(value == null || value >= 0) {
@@ -71,6 +73,7 @@ open class UpstreamsConfig {
copy.validationInterval = firstNonNull(overwrites.validationInterval, this.validationInterval) copy.validationInterval = firstNonNull(overwrites.validationInterval, this.validationInterval)
copy.providesBalance = firstNonNull(overwrites.providesBalance, this.providesBalance) copy.providesBalance = firstNonNull(overwrites.providesBalance, this.providesBalance)
copy.validateSyncing = firstNonNull(overwrites.validateSyncing, this.validateSyncing) copy.validateSyncing = firstNonNull(overwrites.validateSyncing, this.validateSyncing)
copy.validateCalllimit = firstNonNull(overwrites.validateCalllimit, this.validateCalllimit)
copy.timeout = firstNonNull(overwrites.timeout, this.timeout) copy.timeout = firstNonNull(overwrites.timeout, this.timeout)
return copy return copy
} }
@@ -83,7 +86,8 @@ open class UpstreamsConfig {
this.providesBalance, this.providesBalance,
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)!!
) )
companion object { companion object {

View File

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

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()) private val validator: EthereumUpstreamValidator = EthereumUpstreamValidator(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())

View File

@@ -30,7 +30,7 @@ abstract class EthereumLikeUpstream(
role: UpstreamsConfig.UpstreamRole, role: UpstreamsConfig.UpstreamRole,
targets: CallMethods?, targets: CallMethods?,
private val node: QuorumForLabels.QuorumItem?, private val node: QuorumForLabels.QuorumItem?,
chainConfig: ChainsConfig.ChainConfig val chainConfig: ChainsConfig.ChainConfig
) : DefaultUpstream(id, hash, options, role, targets, node, chainConfig) { ) : DefaultUpstream(id, hash, options, role, targets, node, chainConfig) {
private val capabilities = if (options.providesBalance != false) { private val capabilities = if (options.providesBalance != false) {

View File

@@ -24,21 +24,26 @@ import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
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.hex.HexData
import io.emeraldpay.etherjar.rpc.json.SyncingJson import io.emeraldpay.etherjar.rpc.json.SyncingJson
import io.emeraldpay.etherjar.rpc.json.TransactionCallJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.scheduling.concurrent.CustomizableThreadFactory 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.Tuple2 import reactor.util.function.Tuple3
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( open class EthereumUpstreamValidator @JvmOverloads constructor(
private val upstream: Upstream, private val upstream: Upstream,
private val options: UpstreamsConfig.Options private val options: UpstreamsConfig.Options,
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 =
@@ -50,15 +55,54 @@ open class EthereumUpstreamValidator(
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: Tuple2<UpstreamAvailability, UpstreamAvailability>): UpstreamAvailability { fun resolve(results: Tuple3<UpstreamAvailability, UpstreamAvailability, UpstreamAvailability>): UpstreamAvailability {
return if (results.t1.isBetterTo(results.t2)) results.t2 else results.t1 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
// 30d40 — 200_000
HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030d40")
)
)
)
)
.flatMap(JsonRpcResponse::requireResult)
.doOnError {
log.error(
"Node ${upstream.getId()} is 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> {

View File

@@ -7,6 +7,12 @@ chain-settings:
lagging: 1 lagging: 1
chains: chains:
- id: eth - id: eth
call-validate-contract: 0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96
lags:
syncing: 6
lagging: 1
- id: goerli
call-validate-contract: 0xCD9303A1F6da2a68f465A579a24cc2Ee5AE2192f
lags: lags:
syncing: 6 syncing: 6
lagging: 1 lagging: 1

View File

@@ -38,6 +38,7 @@ class ChainsConfigReaderSpec extends Specification {
then: then:
eth.laggingLagSize == 1 eth.laggingLagSize == 1
eth.syncingLagSize == 6 eth.syncingLagSize == 6
eth.callLimitContract == "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"
pol.laggingLagSize == 10 pol.laggingLagSize == 10
pol.syncingLagSize == 20 pol.syncingLagSize == 20
@@ -48,8 +49,5 @@ class ChainsConfigReaderSpec extends Specification {
sep.laggingLagSize == 1 sep.laggingLagSize == 1
sep.syncingLagSize == 10 sep.syncingLagSize == 10
eth.laggingLagSize == 1
eth.syncingLagSize == 6
} }
} }

View File

@@ -450,11 +450,13 @@ class UpstreamsConfigReaderSpec extends Specification {
disableValidation == false disableValidation == false
validateSyncing == true validateSyncing == true
validatePeers == false validatePeers == false
validateCalllimit == true
} }
with(act.upstreams.get(1).options) { with(act.upstreams.get(1).options) {
disableValidation == false disableValidation == false
validateSyncing == false validateSyncing == false
validatePeers == false validatePeers == false
validateCalllimit == false
} }
with(act.upstreams.get(2).options) { with(act.upstreams.get(2).options) {
disableValidation == true disableValidation == true
@@ -633,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 false, 30, Duration.ofSeconds(60), null, true, 1, true, true
) )
} }
} }

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.config.ChainsConfig import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig.Options import io.emeraldpay.dshackle.config.UpstreamsConfig.Options
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
@@ -70,7 +71,7 @@ class EthereumPosRpcUpstreamMock extends EthereumLikeRpcUpstream {
methods, methods,
new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(labels)), new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(labels)),
new ConnectorFactoryMock(api, new EthereumHeadMock()), new ConnectorFactoryMock(api, new EthereumHeadMock()),
ChainsConfig.ChainConfig.default(), ChainConfig.default(),
true true
) )
this.ethereumHeadMock = this.getHead() as EthereumHeadMock this.ethereumHeadMock = this.getHead() as EthereumHeadMock

View File

@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.config.CacheConfig import io.emeraldpay.dshackle.config.CacheConfig
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.reader.EmptyReader import io.emeraldpay.dshackle.reader.EmptyReader

View File

@@ -15,13 +15,17 @@
*/ */
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.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
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.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
@@ -36,18 +40,18 @@ class EthereumUpstreamValidatorSpec extends Specification {
setup: setup:
def validator = new EthereumUpstreamValidator(Stub(EthereumLikeUpstream), UpstreamsConfig.PartialOptions.getDefaults().buildOptions()) def validator = new EthereumUpstreamValidator(Stub(EthereumLikeUpstream), UpstreamsConfig.PartialOptions.getDefaults().buildOptions())
expect: expect:
validator.resolve(Tuples.of(sync, peers)) == exp validator.resolve(Tuples.of(sync, peers, call)) == exp
where: where:
exp | sync | peers exp | sync | peers | call
OK | OK | OK OK | OK | OK | OK
IMMATURE | OK | IMMATURE IMMATURE | OK | IMMATURE | OK
UNAVAILABLE | OK | UNAVAILABLE UNAVAILABLE | OK | UNAVAILABLE | OK
SYNCING | SYNCING | OK SYNCING | SYNCING | OK | OK
SYNCING | SYNCING | IMMATURE SYNCING | SYNCING | IMMATURE | OK
UNAVAILABLE | SYNCING | UNAVAILABLE UNAVAILABLE | SYNCING | UNAVAILABLE | OK
UNAVAILABLE | UNAVAILABLE | OK UNAVAILABLE | UNAVAILABLE | OK | OK
UNAVAILABLE | UNAVAILABLE | IMMATURE UNAVAILABLE | UNAVAILABLE | IMMATURE | OK
UNAVAILABLE | UNAVAILABLE | UNAVAILABLE UNAVAILABLE | UNAVAILABLE | UNAVAILABLE | OK
} }
def "Doesnt check eth_syncing when disabled"() { def "Doesnt check eth_syncing when disabled"() {
@@ -62,7 +66,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def act = validator.validateSyncing().block(Duration.ofSeconds(1)) def act = validator.validateSyncing().block(Duration.ofSeconds(1))
then: then:
act == OK act == OK
0 * up.getApi() 0 * up.getIngressReader()
} }
def "Syncing is OK when false returned from upstream"() { def "Syncing is OK when false returned from upstream"() {
@@ -175,7 +179,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def act = validator.validatePeers().block(Duration.ofSeconds(1)) def act = validator.validatePeers().block(Duration.ofSeconds(1))
then: then:
act == OK act == OK
0 * up.getApi() 0 * up.getIngressReader()
} }
def "Peers is IMMATURE when state returned too few peers"() { def "Peers is IMMATURE when state returned too few peers"() {
@@ -253,4 +257,66 @@ class EthereumUpstreamValidatorSpec extends Specification {
then: then:
act == UNAVAILABLE act == UNAVAILABLE
} }
def "Doesnt check call limit when disabled"() {
setup:
def options = UpstreamsConfig.PartialOptions.getDefaults().tap {
it.validateCalllimit = false
}.buildOptions()
def up = Mock(EthereumLikeUpstream)
def validator = new EthereumUpstreamValidator(up, options)
when:
def act = validator.validateCallLimit().block(Duration.ofSeconds(1))
then:
act == OK
0 * up.getIngressReader()
}
def "Upstream available if not error from call limit check"() {
setup:
def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions()
def up = TestingCommons.upstream(
new ApiReaderMock().tap {
answerOnce("eth_call", [new TransactionCallJson(
Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"),
HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030d40")
)], "0x00000000000000000000")
}
)
def validator = new EthereumUpstreamValidator(up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96")
when:
def act = validator.validateCallLimit().block(Duration.ofSeconds(1))
then:
act == OK
when:
def act2 = validator.validateCallLimit().block(Duration.ofSeconds(1))
then:
act2 == OK
}
def "Upstream not available if error returned on call limit check"() {
setup:
def options = UpstreamsConfig.PartialOptions.getDefaults().buildOptions()
def up = TestingCommons.upstream(
new ApiReaderMock().tap {
answer("eth_call", [new TransactionCallJson(
Address.from("0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96"),
HexData.from("0xd8a26e3a0000000000000000000000000000000000000000000000000000000000030d40")
)], new RpcResponseError(RpcResponseError.CODE_INVALID_REQUEST, "Too long"))
}
)
def validator = new EthereumUpstreamValidator(up, options, "0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96")
when:
def act = validator.validateCallLimit().block(Duration.ofSeconds(1))
then:
act == UNAVAILABLE
when:
def act2 = validator.validateCallLimit().block(Duration.ofSeconds(1))
then:
act2 == UNAVAILABLE
}
} }

View File

@@ -7,6 +7,7 @@ chain-settings:
lagging: 1 lagging: 1
chains: chains:
- id: eth - id: eth
call-validate-contract: 0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96
lags: lags:
syncing: 6 syncing: 6
lagging: 1 lagging: 1

View File

@@ -8,6 +8,7 @@ upstreams:
disable-validation: false disable-validation: false
validate-syncing: true validate-syncing: true
validate-peers: false validate-peers: false
validate-call-limit: true
connection: connection:
ethereum: ethereum:
rpc: rpc:
@@ -19,6 +20,7 @@ upstreams:
disable-validation: false disable-validation: false
validate-syncing: false validate-syncing: false
validate-peers: false validate-peers: false
validate-call-limit: false
connection: connection:
ethereum: ethereum:
rpc: rpc: