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:
@@ -11,6 +11,7 @@ data class ChainsConfig(private val chains: Map<Chain, RawChainConfig>, val curr
|
||||
data class RawChainConfig(
|
||||
var syncingLagSize: Int? = null,
|
||||
var laggingLagSize: Int? = null,
|
||||
var callLimitContract: String? = null,
|
||||
var options: UpstreamsConfig.PartialOptions? = null
|
||||
) {
|
||||
|
||||
@@ -26,11 +27,12 @@ data class ChainsConfig(private val chains: Map<Chain, RawChainConfig>, val curr
|
||||
data class ChainConfig(
|
||||
val syncingLagSize: Int,
|
||||
val laggingLagSize: Int,
|
||||
val options: UpstreamsConfig.PartialOptions
|
||||
val options: UpstreamsConfig.PartialOptions,
|
||||
val callLimitContract: String?
|
||||
) {
|
||||
companion object {
|
||||
@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(
|
||||
laggingLagSize = raw.laggingLagSize ?: default.laggingLagSize ?: 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(
|
||||
syncingLagSize = patch?.syncingLagSize ?: current.syncingLagSize,
|
||||
laggingLagSize = patch?.laggingLagSize ?: current.laggingLagSize,
|
||||
options = patch?.options ?: current.options
|
||||
options = patch?.options ?: current.options,
|
||||
callLimitContract = patch?.callLimitContract ?: current.callLimitContract
|
||||
)
|
||||
|
||||
private fun merge(
|
||||
|
||||
@@ -53,6 +53,9 @@ class ChainsConfigReader(
|
||||
rawConfig.laggingLagSize = it
|
||||
}
|
||||
}
|
||||
getValueAsString(node, "call-validate-contract")?.let {
|
||||
rawConfig.callLimitContract = it
|
||||
}
|
||||
upstreamsConfigReader.tryReadOptions(node)?.let {
|
||||
rawConfig.options = it
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ open class UpstreamsConfig {
|
||||
val providesBalance: Boolean?,
|
||||
val validatePeers: Boolean,
|
||||
val minPeers: Int,
|
||||
val validateSyncing: Boolean
|
||||
val validateSyncing: Boolean,
|
||||
val validateCallLimit: Boolean
|
||||
)
|
||||
|
||||
open class PartialOptions {
|
||||
@@ -51,6 +52,7 @@ open class UpstreamsConfig {
|
||||
var timeout: Duration? = null
|
||||
var providesBalance: Boolean? = null
|
||||
var validatePeers: Boolean? = null
|
||||
var validateCalllimit: Boolean? = null
|
||||
var minPeers: Int? = null
|
||||
set(value) {
|
||||
require(value == null || value >= 0) {
|
||||
@@ -71,6 +73,7 @@ open class UpstreamsConfig {
|
||||
copy.validationInterval = firstNonNull(overwrites.validationInterval, this.validationInterval)
|
||||
copy.providesBalance = firstNonNull(overwrites.providesBalance, this.providesBalance)
|
||||
copy.validateSyncing = firstNonNull(overwrites.validateSyncing, this.validateSyncing)
|
||||
copy.validateCalllimit = firstNonNull(overwrites.validateCalllimit, this.validateCalllimit)
|
||||
copy.timeout = firstNonNull(overwrites.timeout, this.timeout)
|
||||
return copy
|
||||
}
|
||||
@@ -83,7 +86,8 @@ open class UpstreamsConfig {
|
||||
this.providesBalance,
|
||||
firstNonNull(this.validatePeers, true)!!,
|
||||
firstNonNull(this.minPeers, 1)!!,
|
||||
firstNonNull(this.validateSyncing, true)!!
|
||||
firstNonNull(this.validateSyncing, true)!!,
|
||||
firstNonNull(this.validateCalllimit, true)!!
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -353,6 +353,9 @@ class UpstreamsConfigReader(
|
||||
getValueAsBool(values, "validate-syncing")?.let {
|
||||
options.validateSyncing = it
|
||||
}
|
||||
getValueAsBool(values, "validate-call-limit")?.let {
|
||||
options.validateCalllimit = it
|
||||
}
|
||||
getValueAsInt(values, "min-peers")?.let {
|
||||
options.minPeers = it
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
private val validator: EthereumUpstreamValidator = EthereumUpstreamValidator(this, getOptions(), chainConfig.callLimitContract)
|
||||
private val connector: EthereumConnector = connectorFactory.create(this, validator, chain, skipEnhance)
|
||||
private val labelsDetector = EthereumLabelsDetector(this.getIngressReader())
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ abstract class EthereumLikeUpstream(
|
||||
role: UpstreamsConfig.UpstreamRole,
|
||||
targets: CallMethods?,
|
||||
private val node: QuorumForLabels.QuorumItem?,
|
||||
chainConfig: ChainsConfig.ChainConfig
|
||||
val chainConfig: ChainsConfig.ChainConfig
|
||||
) : DefaultUpstream(id, hash, options, role, targets, node, chainConfig) {
|
||||
|
||||
private val capabilities = if (options.providesBalance != false) {
|
||||
|
||||
@@ -24,21 +24,26 @@ import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
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.json.SyncingJson
|
||||
import io.emeraldpay.etherjar.rpc.json.TransactionCallJson
|
||||
import org.slf4j.LoggerFactory
|
||||
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.Tuple2
|
||||
import reactor.util.function.Tuple3
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
open class EthereumUpstreamValidator(
|
||||
open class EthereumUpstreamValidator @JvmOverloads constructor(
|
||||
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 {
|
||||
private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java)
|
||||
val scheduler =
|
||||
@@ -50,15 +55,54 @@ open class EthereumUpstreamValidator(
|
||||
open fun validate(): Mono<UpstreamAvailability> {
|
||||
return Mono.zip(
|
||||
validateSyncing(),
|
||||
validatePeers()
|
||||
validatePeers(),
|
||||
validateCallLimit()
|
||||
)
|
||||
.map(::resolve)
|
||||
.defaultIfEmpty(UpstreamAvailability.UNAVAILABLE)
|
||||
.onErrorReturn(UpstreamAvailability.UNAVAILABLE)
|
||||
}
|
||||
|
||||
fun resolve(results: Tuple2<UpstreamAvailability, UpstreamAvailability>): UpstreamAvailability {
|
||||
return if (results.t1.isBetterTo(results.t2)) results.t2 else results.t1
|
||||
fun resolve(results: Tuple3<UpstreamAvailability, 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
|
||||
// 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> {
|
||||
|
||||
@@ -7,6 +7,12 @@ chain-settings:
|
||||
lagging: 1
|
||||
chains:
|
||||
- id: eth
|
||||
call-validate-contract: 0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96
|
||||
lags:
|
||||
syncing: 6
|
||||
lagging: 1
|
||||
- id: goerli
|
||||
call-validate-contract: 0xCD9303A1F6da2a68f465A579a24cc2Ee5AE2192f
|
||||
lags:
|
||||
syncing: 6
|
||||
lagging: 1
|
||||
|
||||
Reference in New Issue
Block a user