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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user