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()) {
|
) : EthereumLowerBoundDetectorBase(upstream.getChain()) {
|
||||||
private val recursiveLowerBound = RecursiveLowerBound(upstream, LowerBoundType.STATE, stateErrors, lowerBounds, commonErrorPatterns)
|
private val recursiveLowerBound = RecursiveLowerBound(upstream, LowerBoundType.STATE, stateErrors, lowerBounds, commonErrorPatterns)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var supportsStateOverride: Boolean? = null
|
||||||
|
|
||||||
companion object {
|
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(
|
val stateErrors = setOf(
|
||||||
"No state available for block", // nethermind
|
"No state available for block", // nethermind
|
||||||
"missing trie node", // geth
|
"missing trie node", // geth
|
||||||
@@ -66,27 +73,166 @@ class EthereumLowerBoundStateDetector(
|
|||||||
return 3
|
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> {
|
override fun internalDetectLowerBound(): Flux<LowerBoundData> {
|
||||||
return recursiveLowerBound.recursiveDetectLowerBound { block ->
|
return recursiveLowerBound.recursiveDetectLowerBound { block ->
|
||||||
if (block == 0L) {
|
if (block == 0L) {
|
||||||
|
log.debug("Testing block 0 for upstream {} (genesis block)", upstream.getId())
|
||||||
Mono.just(ChainResponse(ByteArray(0), null))
|
Mono.just(ChainResponse(ByteArray(0), null))
|
||||||
} else {
|
} else {
|
||||||
upstream.getIngressReader().read(
|
detectStateForBlock(block)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}.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 {
|
}.flatMap {
|
||||||
Flux.just(it, lowerBoundFrom(it, LowerBoundType.TRACE))
|
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> {
|
override fun types(): Set<LowerBoundType> {
|
||||||
return setOf(LowerBoundType.STATE, LowerBoundType.TRACE)
|
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(),
|
File(this::class.java.getResource("/responses/get-by-number-response.json")!!.toURI()).toPath(),
|
||||||
)
|
)
|
||||||
val reader = mock<ChainReader> {
|
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 {
|
blocks.forEach {
|
||||||
if (it == 17964844L) {
|
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 {
|
on {
|
||||||
read(ChainRequest("eth_getBalance", ListParams(ZERO_ADDRESS, it.toHex())))
|
read(ChainRequest("eth_getBalance", ListParams(ZERO_ADDRESS, it.toHex())))
|
||||||
} doReturn Mono.just(ChainResponse(ByteArray(0), null))
|
} doReturn Mono.just(ChainResponse(ByteArray(0), null))
|
||||||
@@ -56,6 +98,27 @@ class RecursiveLowerBoundServiceTest {
|
|||||||
read(ChainRequest("eth_getTransactionByHash", ListParams("0x99e52a94cfdf83a5bdadcd2e25c71574a5a24fa4df56a33f9f8b5cb6fa0ac657")))
|
read(ChainRequest("eth_getTransactionByHash", ListParams("0x99e52a94cfdf83a5bdadcd2e25c71574a5a24fa4df56a33f9f8b5cb6fa0ac657")))
|
||||||
} doReturn Mono.just(ChainResponse(ByteArray(0), null))
|
} doReturn Mono.just(ChainResponse(ByteArray(0), null))
|
||||||
} else {
|
} 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 {
|
on {
|
||||||
read(ChainRequest("eth_getBalance", ListParams(ZERO_ADDRESS, it.toHex())))
|
read(ChainRequest("eth_getBalance", ListParams(ZERO_ADDRESS, it.toHex())))
|
||||||
} doReturn Mono.error(RuntimeException("missing trie node"))
|
} doReturn Mono.error(RuntimeException("missing trie node"))
|
||||||
@@ -180,6 +243,10 @@ class RecursiveLowerBoundServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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
|
@JvmStatic
|
||||||
fun detectorsFirstBlock(): List<Arguments> = listOf(
|
fun detectorsFirstBlock(): List<Arguments> = listOf(
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
|
|||||||
Reference in New Issue
Block a user