diff --git a/docs/reference-configuration.adoc b/docs/reference-configuration.adoc index 5c8f90d1..f4cfc7ef 100644 --- a/docs/reference-configuration.adoc +++ b/docs/reference-configuration.adoc @@ -772,6 +772,17 @@ If it's set to `0` it essentially disables the peer validation. | Disables checking for the state of syncing on the upstream (as `eth_syncing` method). If the Upstream is in _syncing_ state then the Dshackle doesn't use it for call until it reaches the blockchain head. +| `validate-call-limit` +| boolean +| `true` +| Enable/Disable the call limit validation. Size of call limit is defined by `call-limit` option. +If it's enabled configuration parameter for chain `call-limit-contract` is required. + +| `call-limit-size` +| number +| `1000000` +| For all eth-like chains (except zkSync). The maximum size of the call limit. If the upstream exceeds this limit, it will be considered as failed. + | `timeout` | number | `60` diff --git a/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptions.kt b/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptions.kt index b1920892..4d58d4ad 100644 --- a/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptions.kt +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptions.kt @@ -14,11 +14,12 @@ class ChainOptions { val validateSyncing: Boolean, val validateCallLimit: Boolean, val validateChain: Boolean, + val callLimitSize: Int, ) data class DefaultOptions( var chains: List? = null, - var options: PartialOptions? = null + var options: PartialOptions? = null, ) data class PartialOptions( @@ -28,10 +29,11 @@ class ChainOptions { var timeout: Duration? = null, var providesBalance: Boolean? = null, var validatePeers: Boolean? = null, - var validateCalllimit: Boolean? = null, + var validateCallLimit: Boolean? = null, var minPeers: Int? = null, var validateSyncing: Boolean? = null, - var validateChain: Boolean? = null + var validateChain: Boolean? = null, + var callLimitSize: Int? = null, ) { companion object { @JvmStatic @@ -53,11 +55,12 @@ class ChainOptions { copy.validationInterval = overwrites.validationInterval ?: this.validationInterval copy.providesBalance = overwrites.providesBalance ?: this.providesBalance copy.validateSyncing = overwrites.validateSyncing ?: this.validateSyncing - copy.validateCalllimit = overwrites.validateCalllimit ?: this.validateCalllimit + copy.validateCallLimit = overwrites.validateCallLimit ?: this.validateCallLimit copy.timeout = overwrites.timeout ?: this.timeout copy.validateChain = overwrites.validateChain ?: this.validateChain copy.disableUpstreamValidation = overwrites.disableUpstreamValidation ?: this.disableUpstreamValidation + copy.callLimitSize = overwrites.callLimitSize ?: this.callLimitSize return copy } @@ -71,8 +74,9 @@ class ChainOptions { this.validatePeers ?: true, this.minPeers ?: 1, this.validateSyncing ?: true, - this.validateCalllimit ?: true, + this.validateCallLimit ?: true, this.validateChain ?: true, + this.callLimitSize ?: 1_000_000, ) } } diff --git a/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptionsReader.kt b/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptionsReader.kt index 52dc5850..8b88647d 100644 --- a/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptionsReader.kt +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptionsReader.kt @@ -23,7 +23,7 @@ class ChainOptionsReader : YamlConfigReader() { options.validateSyncing = it } getValueAsBool(values, "validate-call-limit")?.let { - options.validateCalllimit = it + options.validateCallLimit = it } getValueAsBool(values, "validate-chain")?.let { options.validateChain = it @@ -43,6 +43,9 @@ class ChainOptionsReader : YamlConfigReader() { getValueAsBool(values, "balance")?.let { options.providesBalance = it } + getValueAsInt(values, "call-limit-size")?.let { + options.callLimitSize = it + } return options } } diff --git a/foundation/src/main/resources/chains.yaml b/foundation/src/main/resources/chains.yaml index 940a5d63..b4fd0701 100644 --- a/foundation/src/main/resources/chains.yaml +++ b/foundation/src/main/resources/chains.yaml @@ -273,8 +273,6 @@ chain-settings: type: eth settings: expected-block-time: 5s - options: - disable-validation: true lags: syncing: 40 lagging: 20 diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt index 945c1e26..8d37b572 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt @@ -120,31 +120,19 @@ open class EthereumUpstreamValidator @JvmOverloads constructor( } private fun validateCallLimit(): Mono { - if (!options.validateCallLimit || config.callLimitContract == null) { + val validator = callLimitValidatorFactory(upstream, options, config, chain) + if (!validator.isEnabled()) { return Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID) } return upstream.getIngressReader() - .read( - ChainRequest( - "eth_call", - ListParams( - TransactionCallJson( - Address.from(config.callLimitContract), - // calling contract with param 200_000, meaning it will generate 200k symbols or response - // f4240 + metadata — ~1 million - HexData.from("0xd8a26e3a00000000000000000000000000000000000000000000000000000000000f4240"), - ), - "latest", - ), - ), - ) + .read(validator.createRequest()) .flatMap(ChainResponse::requireResult) .map { ValidateUpstreamSettingsResult.UPSTREAM_VALID } .onErrorResume { - if (it.message != null && it.message!!.contains("rpc.returndata.limit")) { + if (validator.isLimitError(it)) { log.warn( "Error: ${it.message}. Node ${upstream.getId()} is probably incorrectly configured. " + - "You need to set up your return limit to at least 1_100_000. " + + "You need to set up your return limit to at least ${options.callLimitSize}. " + "Erigon config example: https://github.com/ledgerwatch/erigon/blob/d014da4dc039ea97caf04ed29feb2af92b7b129d/cmd/utils/flags.go#L369", ) Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR) @@ -221,3 +209,63 @@ open class EthereumUpstreamValidator @JvmOverloads constructor( .flatMap(ChainResponse::requireStringResult) } } + +interface CallLimitValidator { + fun isEnabled(): Boolean + fun createRequest(): ChainRequest + fun isLimitError(err: Throwable): Boolean +} + +class EthCallLimitValidator( + private val options: ChainOptions.Options, + private val config: ChainConfig, +) : CallLimitValidator { + override fun isEnabled() = options.validateCallLimit && config.callLimitContract != null + + override fun createRequest() = ChainRequest( + "eth_call", + ListParams( + TransactionCallJson( + Address.from(config.callLimitContract), + // contract like https://github.com/p2p-org/dshackle/pull/246 + // meta + size in hex + HexData.from("0xd8a26e3a" + options.callLimitSize.toString(16).padStart(64, '0')), + ), + "latest", + ), + ) + + override fun isLimitError(err: Throwable): Boolean = + err.message != null && err.message!!.contains("rpc.returndata.limit") +} + +class ZkSyncCallLimitValidator( + private val upstream: Upstream, + private val options: ChainOptions.Options, +) : CallLimitValidator { + private val method = "debug_traceBlockByNumber" + + override fun isEnabled() = + options.validateCallLimit && upstream.getMethods().getSupportedMethods().contains(method) + + override fun createRequest() = ChainRequest( + method, + ListParams("0x1b73b2b", mapOf("tracer" to "callTracer")), + ) + + override fun isLimitError(err: Throwable): Boolean = + err.message != null && err.message!!.contains("response size should not greater than") +} + +fun callLimitValidatorFactory( + upstream: Upstream, + options: ChainOptions.Options, + config: ChainConfig, + chain: Chain, +): CallLimitValidator { + return if (listOf(Chain.ZKSYNC__MAINNET).contains(chain)) { + ZkSyncCallLimitValidator(upstream, options) + } else { + EthCallLimitValidator(options, config) + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy index 220f3b90..619e21c8 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy @@ -46,7 +46,7 @@ class UpstreamsConfigReaderSpec extends Specification { id == "local" chain == "ethereum" connection instanceof UpstreamsConfig.RpcConnection - with((UpstreamsConfig.RpcConnection)connection) { + with((UpstreamsConfig.RpcConnection) connection) { rpc != null rpc.url == new URI("http://localhost:8545") ws != null @@ -62,7 +62,7 @@ class UpstreamsConfigReaderSpec extends Specification { id == "infura" chain == "ethereum" connection instanceof UpstreamsConfig.RpcConnection - with((UpstreamsConfig.RpcConnection)connection) { + with((UpstreamsConfig.RpcConnection) connection) { rpc.url == new URI("https://mainnet.infura.io/v3/fa28c968191849c1aff541ad1d8511f2") rpc.basicAuth != null with((AuthConfig.ClientBasicAuth) rpc.basicAuth) { @@ -215,7 +215,7 @@ class UpstreamsConfigReaderSpec extends Specification { with(act.upstreams.get(0)) { id == "remote" connection instanceof UpstreamsConfig.GrpcConnection - with((UpstreamsConfig.GrpcConnection)connection) { + with((UpstreamsConfig.GrpcConnection) connection) { host == "10.2.0.15" auth != null with(auth) { @@ -279,7 +279,7 @@ class UpstreamsConfigReaderSpec extends Specification { id == "local" chain == "ethereum" connection instanceof UpstreamsConfig.RpcConnection - with((UpstreamsConfig.RpcConnection)connection) { + with((UpstreamsConfig.RpcConnection) connection) { rpc != null rpc.url == new URI("http://localhost:8545") ws == null @@ -451,13 +451,13 @@ class UpstreamsConfigReaderSpec extends Specification { disableValidation == false validateSyncing == true validatePeers == false - validateCalllimit == true + validateCallLimit == true } with(act.upstreams.get(1).options) { disableValidation == false validateSyncing == false validatePeers == false - validateCalllimit == false + validateCallLimit == false } with(act.upstreams.get(2).options) { disableValidation == true @@ -496,18 +496,18 @@ class UpstreamsConfigReaderSpec extends Specification { result.disableValidation == exp where: - base | overwrite | exp - true | true | true - true | false | false - true | null | true + base | overwrite | exp + true | true | true + true | false | false + true | null | true - false | true | true - false | false | false - false | null | false + false | true | true + false | false | false + false | null | false - null | true | true - null | false | false - null | null | false + null | true | true + null | false | false + null | null | false } def "Merge options for providesBalance"() { @@ -518,18 +518,18 @@ class UpstreamsConfigReaderSpec extends Specification { result.providesBalance == exp where: - base | overwrite | exp - true | true | true - true | false | false - true | null | true + base | overwrite | exp + true | true | true + true | false | false + true | null | true - false | true | true - false | false | false - false | null | false + false | true | true + false | false | false + false | null | false - null | true | true - null | false | false - null | null | null + null | true | true + null | false | false + null | null | null } def "Merge options for validatePeers"() { @@ -540,18 +540,18 @@ class UpstreamsConfigReaderSpec extends Specification { result.validatePeers == exp where: - base | overwrite | exp - true | true | true - true | false | false - true | null | true + base | overwrite | exp + true | true | true + true | false | false + true | null | true - false | true | true - false | false | false - false | null | false + false | true | true + false | false | false + false | null | false - null | true | true - null | false | false - null | null | true + null | true | true + null | false | false + null | null | true } def "Merge options for validateSyncing"() { @@ -562,18 +562,18 @@ class UpstreamsConfigReaderSpec extends Specification { result.validateSyncing == exp where: - base | overwrite | exp - true | true | true - true | false | false - true | null | true + base | overwrite | exp + true | true | true + true | false | false + true | null | true - false | true | true - false | false | false - false | null | false + false | true | true + false | false | false + false | null | false - null | true | true - null | false | false - null | null | true + null | true | true + null | false | false + null | null | true } def "Merge options for timeout"() { @@ -589,12 +589,12 @@ class UpstreamsConfigReaderSpec extends Specification { result.timeout == expValue where: - base | overwrite | exp - 1 | 2 | 2 - 3 | 4 | 4 - 5 | null | 5 - null | 6 | 6 - null | null | null + base | overwrite | exp + 1 | 2 | 2 + 3 | 4 | 4 + 5 | null | 5 + null | 6 | 6 + null | null | null } def "Merge options for minPeers"() { @@ -605,12 +605,12 @@ class UpstreamsConfigReaderSpec extends Specification { result.minPeers == exp where: - base | overwrite | exp - 1 | 2 | 2 - 3 | 4 | 4 - 5 | null | 5 - null | 6 | 6 - null | null | 1 + base | overwrite | exp + 1 | 2 | 2 + 3 | 4 | 4 + 5 | null | 5 + null | 6 | 6 + null | null | 1 } def "Merge options for validationInterval"() { @@ -621,12 +621,12 @@ class UpstreamsConfigReaderSpec extends Specification { result.validationInterval == exp where: - base | overwrite | exp - 1 | 2 | 2 - 3 | 4 | 4 - 5 | null | 5 - null | 6 | 6 - null | null | 30 + base | overwrite | exp + 1 | 2 | 2 + 3 | 4 | 4 + 5 | null | 5 + null | 6 | 6 + null | null | 30 } def "Options with default values"() { @@ -636,7 +636,7 @@ class UpstreamsConfigReaderSpec extends Specification { def options = partialOptions.buildOptions() then: options == new ChainOptions.Options( - false, false, 30, Duration.ofSeconds(60), null, true, 1, true, true, true + false, false, 30, Duration.ofSeconds(60), null, true, 1, true, true, true, 1_000_000 ) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy index 56ac7df1..f3a7d805 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidatorSpec.groovy @@ -274,7 +274,7 @@ class EthereumUpstreamValidatorSpec extends Specification { def "Doesnt validate chan and callLimit when disabled"() { setup: def options = ChainOptions.PartialOptions.getDefaults().tap { - it.validateCalllimit = false + it.validateCallLimit = false it.validateChain = false }.buildOptions() def up = Mock(Upstream) { @@ -344,7 +344,7 @@ class EthereumUpstreamValidatorSpec extends Specification { def "Upstream is valid if chain settings are valid"() { setup: def options = ChainOptions.PartialOptions.getDefaults().tap { - it.validateCalllimit = false + it.validateCallLimit = false }.buildOptions() def up = Mock(Upstream) { 4 * getIngressReader() >> Mock(Reader) { @@ -366,7 +366,7 @@ class EthereumUpstreamValidatorSpec extends Specification { def "Upstream is not valid - specified optimism but got ethereum"() { setup: def options = ChainOptions.PartialOptions.getDefaults().tap { - it.validateCalllimit = false + it.validateCallLimit = false }.buildOptions() def up = Mock(Upstream) { 4 * getIngressReader() >> Mock(Reader) {