state lower bound detection using eth_call (#704)

* state lower bound detection using eth_call

* info logs

* add detection method to log Found state lower bound for upstream

* add runFullLowerBoundDetectionComparison just for compare results and debug

* Revert "add runFullLowerBoundDetectionComparison just for compare results and debug"

This reverts commit 8e9fe6476c62bbe7dc257073dbb37f6c89a131a0.

* revert logs bb34d2b7194427147f42a46ef3b9a8d4598f50a1
This commit is contained in:
Andrey Bronin
2025-08-18 15:36:35 +03:00
committed by GitHub
parent 31b0b8a324
commit 67a8b5cd0d
2 changed files with 223 additions and 10 deletions

View File

@@ -18,7 +18,14 @@ class EthereumLowerBoundStateDetector(
) : EthereumLowerBoundDetectorBase(upstream.getChain()) {
private val recursiveLowerBound = RecursiveLowerBound(upstream, LowerBoundType.STATE, stateErrors, lowerBounds, commonErrorPatterns)
@Volatile
private var supportsStateOverride: Boolean? = null
companion object {
private const val STATE_CHECKER_ADDRESS = "0x1111111111111111111111111111111111111111"
private const val STATE_CHECKER_CALL_DATA = "0x1eaf190c"
private const val STATE_CHECKER_BYTECODE = "0x6080604052348015600e575f5ffd5b50600436106026575f3560e01c80631eaf190c14602a575b5f5ffd5b60306044565b604051603b91906078565b60405180910390f35b5f5f73ffffffffffffffffffffffffffffffffffffffff1631905090565b5f819050919050565b6072816062565b82525050565b5f60208201905060895f830184606b565b9291505056fea2646970667358221220251f5b4d2ed1abe77f66fde198a57ada08562dc3b0afbc6bac0261d1bf516b5d64736f6c634300081e0033"
val stateErrors = setOf(
"No state available for block", // nethermind
"missing trie node", // geth
@@ -66,27 +73,166 @@ class EthereumLowerBoundStateDetector(
return 3
}
private fun testStateOverrideSupport(): Mono<Boolean> {
log.debug("Testing state override support for upstream {}", upstream.getId())
return upstream.getIngressReader().read(
ChainRequest(
"eth_call",
ListParams(
mapOf(
"to" to STATE_CHECKER_ADDRESS,
"data" to STATE_CHECKER_CALL_DATA,
),
"latest",
mapOf(
STATE_CHECKER_ADDRESS to mapOf(
"code" to STATE_CHECKER_BYTECODE,
),
),
),
),
).map { response ->
val supported = if (response.hasResult()) {
val result = String(response.getResult())
result != "\"0x\"" && result != "\"0x0\"" && !response.getResult().contentEquals(Global.nullValue)
} else {
false
}
log.debug("State override support test for upstream {}: {}", upstream.getId(), supported)
supported
}.onErrorResume { error ->
log.debug("State override support test failed for upstream {}: {}", upstream.getId(), error.message)
Mono.just(false)
}.timeout(Defaults.internalCallsTimeout)
}
private fun stateDetectionWithOverride(block: Long): Mono<ChainResponse> {
log.debug("Testing state with override for upstream {} at block {}", upstream.getId(), block)
return upstream.getIngressReader().read(
ChainRequest(
"eth_call",
ListParams(
mapOf(
"to" to STATE_CHECKER_ADDRESS,
"data" to STATE_CHECKER_CALL_DATA,
),
block.toHex(),
mapOf(
STATE_CHECKER_ADDRESS to mapOf(
"code" to STATE_CHECKER_BYTECODE,
),
),
),
),
).doOnNext { response ->
if (response.hasResult() && !response.getResult().contentEquals(Global.nullValue)) {
val result = String(response.getResult())
log.debug("State override successful for upstream {} at block {}: state is available (result: {})", upstream.getId(), block, result)
} else {
log.debug("State override failed for upstream {} at block {}: no state data", upstream.getId(), block)
throw IllegalStateException("No state data")
}
}.doOnError { error ->
log.debug("State override error for upstream {} at block {}: {}", upstream.getId(), block, error.message)
}.timeout(Defaults.internalCallsTimeout)
}
private fun fallbackStateDetection(block: Long): Mono<ChainResponse> {
log.debug("Testing state with fallback (eth_getBalance) for upstream {} at block {}", upstream.getId(), block)
return upstream.getIngressReader().read(
ChainRequest(
"eth_getBalance",
ListParams(ZERO_ADDRESS, block.toHex()),
),
).doOnNext { response ->
if (response.hasResult() && response.getResult().contentEquals(Global.nullValue)) {
log.debug("Fallback state detection failed for upstream {} at block {}: null result", upstream.getId(), block)
throw IllegalStateException("No state data")
} else {
log.debug("Fallback state detection successful for upstream {} at block {}", upstream.getId(), block)
}
}.doOnError { error ->
log.debug("Fallback state detection error for upstream {} at block {}: {}", upstream.getId(), block, error.message)
}.timeout(Defaults.internalCallsTimeout)
}
override fun internalDetectLowerBound(): Flux<LowerBoundData> {
return recursiveLowerBound.recursiveDetectLowerBound { block ->
if (block == 0L) {
log.debug("Testing block 0 for upstream {} (genesis block)", upstream.getId())
Mono.just(ChainResponse(ByteArray(0), null))
} else {
upstream.getIngressReader().read(
ChainRequest(
"eth_getBalance",
ListParams(ZERO_ADDRESS, block.toHex()),
),
).timeout(Defaults.internalCallsTimeout)
}.doOnNext {
if (it.hasResult() && it.getResult().contentEquals(Global.nullValue)) {
throw IllegalStateException("No state data")
}
detectStateForBlock(block)
}
}.doOnNext { lowerBoundData ->
val detectionMethod = when (supportsStateOverride) {
true -> "state override (eth_call)"
false -> "fallback (eth_getBalance)"
null -> "unknown method"
}
log.info("Found state lower bound for upstream {} using {}: block {}", upstream.getId(), detectionMethod, lowerBoundData.lowerBound)
}.flatMap {
Flux.just(it, lowerBoundFrom(it, LowerBoundType.TRACE))
}
}
private fun detectStateForBlock(block: Long): Mono<ChainResponse> {
val currentSupportsStateOverride = supportsStateOverride
return when (currentSupportsStateOverride) {
true -> {
log.debug("Using state override for upstream {} at block {} (cached: supported)", upstream.getId(), block)
stateDetectionWithOverride(block).onErrorResume { error ->
log.debug(
"State override failed for upstream {} at block {}, falling back to eth_getBalance: {}",
upstream.getId(),
block,
error.message,
)
fallbackStateDetection(block)
}
}
false -> {
log.debug("Using fallback method for upstream {} at block {} (cached: not supported)", upstream.getId(), block)
fallbackStateDetection(block)
}
null -> {
log.debug("Testing state override support for upstream {} (first time)", upstream.getId())
testStateOverrideSupport()
.doOnNext { supported ->
supportsStateOverride = supported
log.info("State override support for upstream {}: {}", upstream.getId(), supported)
}
.flatMap { supported ->
if (supported) {
log.debug("Using state override for upstream {} at block {} (newly detected)", upstream.getId(), block)
stateDetectionWithOverride(block).onErrorResume { error ->
log.debug(
"State override failed for upstream {} at block {}, falling back to eth_getBalance: {}",
upstream.getId(),
block,
error.message,
)
fallbackStateDetection(block)
}
} else {
log.debug("Using fallback method for upstream {} at block {} (newly detected)", upstream.getId(), block)
fallbackStateDetection(block)
}
}
.onErrorResume { error ->
log.warn(
"State override support test failed for upstream {}, falling back to eth_getBalance: {}",
upstream.getId(),
error.message,
)
supportsStateOverride = false
fallbackStateDetection(block)
}
}
}
}
override fun types(): Set<LowerBoundType> {
return setOf(LowerBoundType.STATE, LowerBoundType.TRACE)
}

View File

@@ -44,8 +44,50 @@ class RecursiveLowerBoundServiceTest {
File(this::class.java.getResource("/responses/get-by-number-response.json")!!.toURI()).toPath(),
)
val reader = mock<ChainReader> {
// Mock support test for state override (using "latest")
on {
read(
ChainRequest(
"eth_call",
ListParams(
mapOf(
"to" to STATE_CHECKER_ADDRESS,
"data" to STATE_CHECKER_CALL_DATA,
),
"latest",
mapOf(
STATE_CHECKER_ADDRESS to mapOf(
"code" to STATE_CHECKER_BYTECODE,
),
),
),
),
)
} doReturn Mono.just(ChainResponse("\"0x42\"".toByteArray(), null)) // Return non-zero balance to indicate support
blocks.forEach {
if (it == 17964844L) {
// Mock successful state override for the found block
on {
read(
ChainRequest(
"eth_call",
ListParams(
mapOf(
"to" to STATE_CHECKER_ADDRESS,
"data" to STATE_CHECKER_CALL_DATA,
),
it.toHex(),
mapOf(
STATE_CHECKER_ADDRESS to mapOf(
"code" to STATE_CHECKER_BYTECODE,
),
),
),
),
)
} doReturn Mono.just(ChainResponse("\"0x0\"".toByteArray(), null)) // Return 0x0 but this should still be treated as success
on {
read(ChainRequest("eth_getBalance", ListParams(ZERO_ADDRESS, it.toHex())))
} doReturn Mono.just(ChainResponse(ByteArray(0), null))
@@ -56,6 +98,27 @@ class RecursiveLowerBoundServiceTest {
read(ChainRequest("eth_getTransactionByHash", ListParams("0x99e52a94cfdf83a5bdadcd2e25c71574a5a24fa4df56a33f9f8b5cb6fa0ac657")))
} doReturn Mono.just(ChainResponse(ByteArray(0), null))
} else {
// Mock failed state override for other blocks
on {
read(
ChainRequest(
"eth_call",
ListParams(
mapOf(
"to" to STATE_CHECKER_ADDRESS,
"data" to STATE_CHECKER_CALL_DATA,
),
it.toHex(),
mapOf(
STATE_CHECKER_ADDRESS to mapOf(
"code" to STATE_CHECKER_BYTECODE,
),
),
),
),
)
} doReturn Mono.error(RuntimeException("missing trie node"))
on {
read(ChainRequest("eth_getBalance", ListParams(ZERO_ADDRESS, it.toHex())))
} doReturn Mono.error(RuntimeException("missing trie node"))
@@ -180,6 +243,10 @@ class RecursiveLowerBoundServiceTest {
}
companion object {
private const val STATE_CHECKER_ADDRESS = "0x1111111111111111111111111111111111111111"
private const val STATE_CHECKER_CALL_DATA = "0x1eaf190c"
private const val STATE_CHECKER_BYTECODE = "0x6080604052348015600e575f5ffd5b50600436106026575f3560e01c80631eaf190c14602a575b5f5ffd5b60306044565b604051603b91906078565b60405180910390f35b5f5f73ffffffffffffffffffffffffffffffffffffffff1631905090565b5f819050919050565b6072816062565b82525050565b5f60208201905060895f830184606b565b9291505056fea2646970667358221220251f5b4d2ed1abe77f66fde198a57ada08562dc3b0afbc6bac0261d1bf516b5d64736f6c634300081e0033"
@JvmStatic
fun detectorsFirstBlock(): List<Arguments> = listOf(
Arguments.of(