diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundStateDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundStateDetector.kt index 835808e4..54284cb0 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundStateDetector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumLowerBoundStateDetector.kt @@ -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 { + 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 { + 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 { + 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 { 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 { + 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 { return setOf(LowerBoundType.STATE, LowerBoundType.TRACE) } diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/RecursiveLowerBoundServiceTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/RecursiveLowerBoundServiceTest.kt index 144ad911..a5fd2bc4 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/RecursiveLowerBoundServiceTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/lowerbound/RecursiveLowerBoundServiceTest.kt @@ -44,8 +44,50 @@ class RecursiveLowerBoundServiceTest { File(this::class.java.getResource("/responses/get-by-number-response.json")!!.toURI()).toPath(), ) val reader = mock { + // 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 = listOf( Arguments.of(