diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/ErrorProcessing.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/ErrorProcessing.kt new file mode 100644 index 000000000..fbf5db561 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/ErrorProcessing.kt @@ -0,0 +1,43 @@ +package io.emeraldpay.dshackle.rpc + +import org.springframework.stereotype.Component + +interface ErrorProcessor { + + fun matches(result: NativeCall.CallResult): Boolean + + fun errorProcess(error: NativeCall.CallError): NativeCall.CallError +} + +@Component +open class ErrorCorrector( + private val processors: List, +) { + + fun correctError(result: NativeCall.CallResult): NativeCall.CallError { + if (!result.isError()) { + throw IllegalStateException("No error to correct") + } + return processors + .firstOrNull { it.matches(result) } + ?.errorProcess(result.error!!) + ?: result.error!! + } +} + +@Component +class NethermindEthCallRevertedErrorProcessor : ErrorProcessor { + override fun matches(result: NativeCall.CallResult): Boolean { + return result.ctx?.payload?.method == "eth_call" && (result.error?.data?.startsWith("Reverted") ?: false) + } + + override fun errorProcess(error: NativeCall.CallError): NativeCall.CallError { + return NativeCall.CallError( + 3, + error.message, + error.upstreamError, + error.data?.removePrefix("Reverted "), + error.upstreamId, + ) + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt index fb8dde178..6641addbd 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt @@ -65,6 +65,7 @@ open class NativeCall( private val signer: ResponseSigner, config: MainConfig, private val tracer: Tracer, + private val errorCorrector: ErrorCorrector, ) { private val log = LoggerFactory.getLogger(NativeCall::class.java) @@ -173,11 +174,12 @@ open class NativeCall( .setSucceed(!it.isError()) .setId(it.id) if (it.isError()) { - it.error?.let { error -> - result.setErrorMessage(error.message) - .setErrorCode(error.id) + it.error?.let { _ -> + val fixedError = errorCorrector.correctError(it) + result.setErrorMessage(fixedError.message) + .setErrorCode(fixedError.id) - error.data?.let { data -> + fixedError.data?.let { data -> result.setErrorData(data) } } @@ -566,7 +568,7 @@ open class NativeCall( open class CallFailure(val id: Int, val reason: Throwable) : Exception("Failed to call $id: ${reason.message}") - open class CallError( + data class CallError( val id: Int, val message: String, val upstreamError: JsonRpcError?, diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy index 06fb63bb1..576534514 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy @@ -70,7 +70,7 @@ class NativeCallSpec extends Specification { config.cache = cacheConfig config.passthrough = passthrough - new NativeCall(upstreams, signer, config, Stub(Tracer)) + new NativeCall(upstreams, signer, config, Stub(Tracer), Stub(ErrorCorrector)) } def "Tries router first"() { diff --git a/src/test/kotlin/io/emeraldpay/dshackle/rpc/ErrorProcessingTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/rpc/ErrorProcessingTest.kt new file mode 100644 index 000000000..e2a4fa3a1 --- /dev/null +++ b/src/test/kotlin/io/emeraldpay/dshackle/rpc/ErrorProcessingTest.kt @@ -0,0 +1,93 @@ +package io.emeraldpay.dshackle.rpc + +import io.emeraldpay.dshackle.quorum.CallQuorum +import io.emeraldpay.dshackle.upstream.Multistream +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.mock + +class ErrorProcessingTest { + + @Test + fun `fix nethermind eth_call reverted error`() { + val result = result("eth_call", "Reverted 0x0111") + val error = result.error!! + val corrector = ErrorCorrector(listOf(NethermindEthCallRevertedErrorProcessor())) + + val fixedError = corrector.correctError(result) + + assertEquals( + NativeCall.CallError(3, error.message, error.upstreamError, "0x0111", error.upstreamId), + fixedError, + ) + } + + @Test + fun `return the same error if there is no suitable processor`() { + val result = result("eth_getBlockByNumber", "Reverted 0x0111") + val error = result.error!! + val corrector = ErrorCorrector(listOf(NethermindEthCallRevertedErrorProcessor())) + + val fixedError = corrector.correctError(result) + + assertEquals( + NativeCall.CallError(55, error.message, error.upstreamError, "Reverted 0x0111", error.upstreamId), + fixedError, + ) + } + + @Test + fun `throw an exception if there is no error in result`() { + val result = NativeCall.CallResult( + 1, + 2, + null, + null, + null, + null, + ) + assertThrows("No error to correct") { + val corrector = ErrorCorrector(listOf(NethermindEthCallRevertedErrorProcessor())) + corrector.correctError(result) + } + } + + @Test + fun `NethermindEthCallRevertedErrorProcessor returns false if result is with null data`() { + val processor = NethermindEthCallRevertedErrorProcessor() + val result = result("eth_call", null) + + val matched = processor.matches(result) + + assertFalse(matched) + } + + private fun result(method: String, errorData: String?): NativeCall.CallResult = + NativeCall.CallResult( + 1, + 2, + null, + NativeCall.CallError( + 55, + "reverted", + JsonRpcError(1, "errMessage", null), + errorData, + "upId", + ), + null, + NativeCall.ValidCallContext( + 1, + 2, + mock(), + Selector.empty, + mock(), + NativeCall.ParsedCallDetails(method, emptyList()), + "req", + 1, + ), + ) +}