erigon bug detector (#683)

This commit is contained in:
Vyacheslav
2025-07-14 16:51:43 +02:00
committed by GitHub
parent c025cbd840
commit b940ce9efa
5 changed files with 96 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ class ChainOptions {
val callLimitSize: Int, val callLimitSize: Int,
val disableLivenessSubscriptionValidation: Boolean, val disableLivenessSubscriptionValidation: Boolean,
val disableBoundValidation: Boolean = false, val disableBoundValidation: Boolean = false,
val valdateErigonBug: Boolean,
) )
data class DefaultOptions( data class DefaultOptions(
@@ -40,6 +41,7 @@ class ChainOptions {
var callLimitSize: Int? = null, var callLimitSize: Int? = null,
var disableLivenessSubscriptionValidation: Boolean? = null, var disableLivenessSubscriptionValidation: Boolean? = null,
var disableBoundValidation: Boolean? = null, var disableBoundValidation: Boolean? = null,
var validateErigonBug: Boolean? = null
) { ) {
companion object { companion object {
@JvmStatic @JvmStatic
@@ -70,6 +72,7 @@ class ChainOptions {
copy.callLimitSize = overwrites.callLimitSize ?: this.callLimitSize copy.callLimitSize = overwrites.callLimitSize ?: this.callLimitSize
copy.disableLivenessSubscriptionValidation = overwrites.disableLivenessSubscriptionValidation ?: this.disableLivenessSubscriptionValidation copy.disableLivenessSubscriptionValidation = overwrites.disableLivenessSubscriptionValidation ?: this.disableLivenessSubscriptionValidation
copy.disableBoundValidation = overwrites.disableBoundValidation ?: this.disableBoundValidation copy.disableBoundValidation = overwrites.disableBoundValidation ?: this.disableBoundValidation
copy.validateErigonBug = overwrites.validateErigonBug ?: this.validateErigonBug
return copy return copy
} }
@@ -89,6 +92,7 @@ class ChainOptions {
this.callLimitSize ?: 1_000_000, this.callLimitSize ?: 1_000_000,
this.disableLivenessSubscriptionValidation ?: false, this.disableLivenessSubscriptionValidation ?: false,
this.disableBoundValidation ?: false, this.disableBoundValidation ?: false,
this.validateErigonBug ?: true,
) )
} }
} }

View File

@@ -157,6 +157,9 @@ object EthereumChainSpecific : AbstractPollChainSpecific() {
val validators = mutableListOf<SingleValidator<ValidateUpstreamSettingsResult>>( val validators = mutableListOf<SingleValidator<ValidateUpstreamSettingsResult>>(
ChainIdValidator(upstream, chain), ChainIdValidator(upstream, chain),
) )
if (options.valdateErigonBug) {
validators.add(ErigonBuggedValidator(upstream))
}
val limitValidator = EthCallLimitValidator(upstream, options, config) val limitValidator = EthCallLimitValidator(upstream, options, config)
if (limitValidator.isEnabled()) { if (limitValidator.isEnabled()) {
validators.add(limitValidator) validators.add(limitValidator)

View File

@@ -226,3 +226,90 @@ class GasPriceValidator(
} }
} }
} }
class ErigonBuggedValidator(
private val upstream: Upstream,
) : SingleValidator<ValidateUpstreamSettingsResult> {
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<ValidateUpstreamSettingsResult> {
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<Boolean> =
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<BigInteger> =
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<Boolean> {
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) }
}
}

View File

@@ -636,7 +636,7 @@ class UpstreamsConfigReaderSpec extends Specification {
def options = partialOptions.buildOptions() def options = partialOptions.buildOptions()
then: then:
options == new ChainOptions.Options( 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
) )
} }
} }

View File

@@ -210,7 +210,7 @@ class GenericRpcHeadTest {
val upstream = mock<DefaultUpstream> { val upstream = mock<DefaultUpstream> {
on { getIngressReader() } doReturn reader on { getIngressReader() } doReturn reader
on { getChain() } doReturn Chain.ETHEREUM__MAINNET 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( val head = GenericRpcHead(
reader, reader,