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 64157d2a..645d04a0 100644 --- a/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptions.kt +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/foundation/ChainOptions.kt @@ -18,6 +18,7 @@ class ChainOptions { val callLimitSize: Int, val disableLivenessSubscriptionValidation: Boolean, val disableBoundValidation: Boolean = false, + val valdateErigonBug: Boolean, ) data class DefaultOptions( @@ -40,6 +41,7 @@ class ChainOptions { var callLimitSize: Int? = null, var disableLivenessSubscriptionValidation: Boolean? = null, var disableBoundValidation: Boolean? = null, + var validateErigonBug: Boolean? = null ) { companion object { @JvmStatic @@ -70,6 +72,7 @@ class ChainOptions { copy.callLimitSize = overwrites.callLimitSize ?: this.callLimitSize copy.disableLivenessSubscriptionValidation = overwrites.disableLivenessSubscriptionValidation ?: this.disableLivenessSubscriptionValidation copy.disableBoundValidation = overwrites.disableBoundValidation ?: this.disableBoundValidation + copy.validateErigonBug = overwrites.validateErigonBug ?: this.validateErigonBug return copy } @@ -89,6 +92,7 @@ class ChainOptions { this.callLimitSize ?: 1_000_000, this.disableLivenessSubscriptionValidation ?: false, this.disableBoundValidation ?: false, + this.validateErigonBug ?: true, ) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt index e9fce6bd..ca906214 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt @@ -157,6 +157,9 @@ object EthereumChainSpecific : AbstractPollChainSpecific() { val validators = mutableListOf>( ChainIdValidator(upstream, chain), ) + if (options.valdateErigonBug) { + validators.add(ErigonBuggedValidator(upstream)) + } val limitValidator = EthCallLimitValidator(upstream, options, config) if (limitValidator.isEnabled()) { validators.add(limitValidator) 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 1fc90dad..c467b668 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt @@ -226,3 +226,90 @@ class GasPriceValidator( } } } + +class ErigonBuggedValidator( + private val upstream: Upstream, +) : SingleValidator { + + companion object { + @JvmStatic + val log: Logger = LoggerFactory.getLogger(ErigonBuggedValidator::class.java) + + private const val ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + private val FIVE_THOUSAND = BigInteger.valueOf(5_000) + } + + override fun validate(onError: ValidateUpstreamSettingsResult): Mono { + return isErigon().flatMap { isErigon -> + if (!isErigon) { + Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID) + } else { + latestBlockNumber().flatMap { latest -> + val pastBlock = latest.subtract(FIVE_THOUSAND).max(BigInteger.ZERO) + + Mono.zip( + balanceOkAt(pastBlock), // must succeed + balanceOkAt(BigInteger.ONE) // must fail + ).map { checks -> + val isBugged = checks.t1 && !checks.t2 + if (isBugged) { + log.warn( + "Erigon balance-bug detected on upstream {}: ok @ {}, error @ 0x1", + upstream.getId(), pastBlock + ) + ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR + } else { + ValidateUpstreamSettingsResult.UPSTREAM_VALID + } + } + } + } + }.onErrorResume { ex -> + log.error( + "Irrecoverable error during Erigon bug validation for {}, reason - {}", + upstream.getId(), ex.message + ) + Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR) + } + } + + private fun isErigon(): Mono = + upstream.getIngressReader() + .read(ChainRequest("web3_clientVersion", ListParams())) + .retryRandomBackoff(3, Duration.ofMillis(100), Duration.ofMillis(500)) { ctx -> + log.warn( + "error during clientVersion retrieving for {}, iteration {}, reason - {}", + upstream.getId(), ctx.iteration(), ctx.exception().message + ) + } + .flatMap(ChainResponse::requireStringResult) + .map { it.lowercase().contains("erigon") } + .doOnError { log.error("Error during execution 'web3_clientVersion' - {} for {}", it.message, upstream.getId()) } + + private fun latestBlockNumber(): Mono = + upstream.getIngressReader() + .read(ChainRequest("eth_blockNumber", ListParams())) + .retryRandomBackoff(3, Duration.ofMillis(100), Duration.ofMillis(500)) { ctx -> + log.warn( + "error during blockNumber retrieving for {}, iteration {}, reason - {}", + upstream.getId(), ctx.iteration(), ctx.exception().message + ) + } + .flatMap(ChainResponse::requireStringResult) + .map { BigInteger(it.removePrefix("0x"), 16) } + .doOnError { log.error("Error during execution 'eth_blockNumber' - {} for {}", it.message, upstream.getId()) } + + private fun balanceOkAt(blockNumber: BigInteger): Mono { + val tag = "0x${blockNumber.toString(16)}" + return upstream.getIngressReader() + .read(ChainRequest("eth_getBalance", ListParams(ZERO_ADDRESS, tag))) + .retryRandomBackoff(3, Duration.ofMillis(100), Duration.ofMillis(500)) { ctx -> + log.warn( + "error during balance retrieving for {}, block {}, iteration {}, reason - {}", + upstream.getId(), tag, ctx.iteration(), ctx.exception().message + ) + } + .flatMap { resp -> resp.requireStringResult().map { true } } + .onErrorResume { Mono.just(false) } + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy index 434cc4a4..92d85b56 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy @@ -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, true, 1_000_000, false, false + false, false, 30, Duration.ofSeconds(60), null, true, 1, true, true, true, true, 1_000_000, false, false, true ) } } diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericRpcHeadTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericRpcHeadTest.kt index 08df41b0..22511100 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericRpcHeadTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericRpcHeadTest.kt @@ -210,7 +210,7 @@ class GenericRpcHeadTest { val upstream = mock { on { getIngressReader() } doReturn reader on { getChain() } doReturn Chain.ETHEREUM__MAINNET - on { getOptions() } doReturn ChainOptions.PartialOptions().buildOptions() + on { getOptions() } doReturn ChainOptions.PartialOptions().apply { this.validateErigonBug = false }.buildOptions() } val head = GenericRpcHead( reader,