diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt index 0cdf0970..368ddff4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt @@ -88,4 +88,7 @@ abstract class AbstractHead : Head { return head.get() } + override fun getCurrentHeight(): Long? { + return getCurrent()?.height + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt index db0f97ea..14910d83 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt @@ -28,4 +28,8 @@ class EmptyHead : Head { override fun onBeforeBlock(handler: Runnable) { } + + override fun getCurrentHeight(): Long? { + return null + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt index ffac632f..7bc06854 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt @@ -36,4 +36,6 @@ interface Head { * @see getFlux */ fun onBeforeBlock(handler: Runnable) + + fun getCurrentHeight(): Long? } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumMultistream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumMultistream.kt index 6da70f0a..177e049a 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumMultistream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumMultistream.kt @@ -117,7 +117,7 @@ open class EthereumMultistream( } override fun getRoutedApi(matcher: Selector.Matcher): Mono> { - return Mono.just(NativeCallRouter(reader, getMethods())) + return Mono.just(NativeCallRouter(reader, getMethods(), getHead())) } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumReader.kt index caa27685..6b4a38c9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumReader.kt @@ -115,14 +115,14 @@ open class EthereumReader( ) } - fun blocksByIdAsCont(): Reader { + open fun blocksByIdAsCont(): Reader { return TransformingReader( blocksById(), blockAsContainer ) } - fun blocksByHeightAsCont(): Reader { + open fun blocksByHeightAsCont(): Reader { return CompoundReader( caches.getBlocksByHeight(), directReader.blockByHeightReader @@ -139,7 +139,7 @@ open class EthereumReader( ) } - fun txByHashAsCont(): Reader { + open fun txByHashAsCont(): Reader { return CompoundReader( caches.getTxByHash(), RekeyingReader(idToTxHash, directReader.txReader) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouter.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouter.kt index 72c0046f..c7637ebc 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouter.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouter.kt @@ -15,11 +15,10 @@ */ package io.emeraldpay.dshackle.upstream.ethereum -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse @@ -32,7 +31,8 @@ import java.math.BigInteger class NativeCallRouter( private val reader: EthereumReader, - private val methods: CallMethods + private val methods: CallMethods, + private val head: Head ) : Reader { companion object { @@ -98,12 +98,22 @@ class NativeCallRouter( } } method == "eth_getBlockByNumber" -> { - if (params.size != 2) { - throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters") - } - val number: Long - try { - val quantity = HexQuantity.from(params[0].toString()) ?: throw IllegalArgumentException() + getBlockByNumber(params) + } + else -> null + } + } + + fun getBlockByNumber(params: List): Mono? { + if (params.size != 2) { + throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters") + } + val number: Long + try { + val blockRef = params[0].toString() + when { + blockRef.startsWith("0x") -> { + val quantity = HexQuantity.from(blockRef) ?: throw IllegalArgumentException() number = quantity.value.let { if (it < BigInteger.valueOf(Long.MAX_VALUE) && it >= BigInteger.ZERO) { it.toLong() @@ -111,18 +121,29 @@ class NativeCallRouter( throw IllegalArgumentException() } } - } catch (e: IllegalArgumentException) { - throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block number") } - val withTx = params[1].toString().toBoolean() - if (withTx) { - log.warn("Block by number is not implemented") - null - } else { - reader.blocksByHeightAsCont().read(number).map { it.json!! } + blockRef == "latest" -> { + number = head.getCurrentHeight() ?: return null + } + blockRef == "earliest" -> { + number = 0 + } + blockRef == "pending" -> { + return null + } + else -> { + throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Block number is invalid") } } - else -> null + } catch (e: IllegalArgumentException) { + throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block number") + } + val withTx = params[1].toString().toBoolean() + return if (withTx) { + log.warn("Block by number is not implemented") + null + } else { + reader.blocksByHeightAsCont().read(number).map { it.json!! } } } } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy index 36dd9d33..7b8f089d 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy @@ -62,4 +62,9 @@ class EthereumHeadMock implements Head { void onBeforeBlock(@NotNull Runnable handler) { handlers.add(handler) } + + @Override + Long getCurrentHeight() { + return latest?.height + } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy index 9b6518ce..d51d9b19 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy @@ -21,6 +21,8 @@ import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.config.CacheConfig +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.reader.EmptyReader import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.Multistream @@ -30,12 +32,17 @@ import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain +import io.infinitape.etherjar.domain.BlockHash +import io.infinitape.etherjar.rpc.json.BlockJson + +import java.time.Instant class TestingCommons { static EthereumApiMock api() { return new EthereumApiMock() } + static EthereumUpstreamMock upstream(Reader api) { return new EthereumUpstreamMock(Chain.ETHEREUM, api) } @@ -69,4 +76,15 @@ class TestingCommons { static FileResolver fileResolver() { return new FileResolver(new File("src/test/resources")) } + + static BlockContainer blockForEthereum(Long height) { + BlockJson block = new BlockJson().tap { + setNumber(height) + setHash(BlockHash.from("0xc4b01774e426325b50f0c709753ec7cf1f1774439d587dfb91f2a4eeb8179cde")) + setTotalDifficulty(BigInteger.ONE) + setTimestamp(Instant.now()) + } + return BlockContainer.from(block) + } + } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouterSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouterSpec.groovy index 4e3bac26..dd2270e5 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouterSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouterSpec.groovy @@ -1,11 +1,18 @@ package io.emeraldpay.dshackle.upstream.ethereum +import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.cache.Caches +import io.emeraldpay.dshackle.reader.EmptyReader import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.EmptyHead +import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.grpc.Chain +import io.infinitape.etherjar.rpc.json.BlockJson import org.apache.commons.collections4.functors.ConstantFactory +import reactor.core.publisher.Mono import spock.lang.Specification import java.time.Duration @@ -21,11 +28,92 @@ class NativeCallRouterSpec extends Specification { Caches.default(), ConstantFactory.constantFactory(new DefaultEthereumMethods(Chain.ETHEREUM)) ), - methods + methods, + new EmptyHead() ) when: def act = router.read(new JsonRpcRequest("eth_coinbase", [])).block(Duration.ofSeconds(1)) then: act.resultAsProcessedString == "0x0000000000000000000000000000000000000000" } + + def "getBlockByNumber with latest uses latest id"() { + setup: + def head = Mock(Head) { + 1 * getCurrentHeight() >> 101L + } + def reader = Mock(EthereumReader) { + _ * blocksByIdAsCont() >> new EmptyReader<>() + _ * txByHashAsCont() >> new EmptyReader<>() + 1 * blocksByHeightAsCont() >> Mock(Reader) { + 1 * read(101L) >> Mono.just(TestingCommons.blockForEthereum(101L)) + } + } + def methods = new DefaultEthereumMethods(Chain.ETHEREUM) + def router = new NativeCallRouter(reader, methods, head) + + when: + def act = router.getBlockByNumber(["latest", false]) + + then: + act != null + with(act.block()) { + it.length > 0 + with(Global.objectMapper.readValue(it, BlockJson)) { + number == 101 + } + } + } + + def "getBlockByNumber with earliest uses 0 block"() { + setup: + def head = Stub(Head) {} + def reader = Mock(EthereumReader) { + _ * blocksByIdAsCont() >> new EmptyReader<>() + _ * txByHashAsCont() >> new EmptyReader<>() + 1 * blocksByHeightAsCont() >> Mock(Reader) { + 1 * read(0L) >> Mono.just(TestingCommons.blockForEthereum(0L)) + } + } + def methods = new DefaultEthereumMethods(Chain.ETHEREUM) + def router = new NativeCallRouter(reader, methods, head) + + when: + def act = router.getBlockByNumber(["earliest", false]) + + then: + act != null + with(act.block()) { + it.length > 0 + with(Global.objectMapper.readValue(it, BlockJson)) { + number == 0 + } + } + } + + def "getBlockByNumber fetches the block"() { + setup: + def head = Stub(Head) {} + def reader = Mock(EthereumReader) { + _ * blocksByIdAsCont() >> new EmptyReader<>() + _ * txByHashAsCont() >> new EmptyReader<>() + 1 * blocksByHeightAsCont() >> Mock(Reader) { + 1 * read(74735L) >> Mono.just(TestingCommons.blockForEthereum(74735L)) + } + } + def methods = new DefaultEthereumMethods(Chain.ETHEREUM) + def router = new NativeCallRouter(reader, methods, head) + + when: + def act = router.getBlockByNumber(["0x123ef", false]) + + then: + act != null + with(act.block()) { + it.length > 0 + with(Global.objectMapper.readValue(it, BlockJson)) { + number == 74735 + } + } + } }