diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelector.kt index 5cb354a5..33eb6775 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelector.kt @@ -63,13 +63,15 @@ class EthereumCallSelector( */ fun getMatcher(method: String, params: String, head: Head, passthrough: Boolean): Mono { if (!passthrough && Collections.binarySearch(TAG_METHODS, method) >= 0) { - return blockTagSelector(params, 1, head) + return blockTagSelector(params, 1, null, head) } else if (!passthrough && method == "eth_getStorageAt") { - return blockTagSelector(params, 2, head) + return blockTagSelector(params, 2, null, head) } else if (method in DefaultEthereumMethods.withFilterIdMethods) { return sameUpstreamMatcher(params) } else if (method in GET_BY_HASH_OR_NUMBER_METHODS) { - return blockMethodSelector(method, params) + return blockMethodSelector(method, params, head) + } else if (method == "eth_getLogs") { + return blockTagSelector(params, 0, "toBlock", head) } return Mono.empty() } @@ -87,36 +89,52 @@ class EthereumCallSelector( val nodeId = hashHex.toInt(16) return Mono.just(Selector.SameNodeMatcher(nodeId.toByte())) } - - private fun blockTagSelector(params: String, pos: Int, head: Head): Mono { + private fun blockTagSelector(params: String, pos: Int, paramName: String?, head: Head): Mono { val list = objectMapper.readerFor(Any::class.java).readValues(params).readAll() if (list.size < pos + 1) { log.debug("Tag is not specified. Ignoring") return Mono.empty() } + // integer block number, a string "latest", "earliest" or "pending", or an object with block reference - val minHeight: Long? = when (val tag = Objects.toString(list[pos])) { + val blockTag = Objects.toString(list[pos]) + + return if (blockTag.startsWith("{") && list[pos] is Map<*, *>) { + val obj = list[pos] as Map<*, *> + when { + paramName != null -> { + return if (obj.containsKey(paramName)) { + blockSelectorByTag(obj[paramName].toString(), head) + } else { + Mono.empty() + } + } + obj.containsKey("blockNumber") -> { + return blockSelectorByTag(obj["blockNumber"].toString(), head) + } + obj.containsKey("blockHash") -> { + return blockSelectorByTag(obj["blockHash"].toString(), head) + } + else -> { + log.debug("Tag is not found. Ignoring") + Mono.empty() + } + } + } else { + blockSelectorByTag(blockTag, head) + } + } + + private fun blockSelectorByTag(tag: String, head: Head): Mono { + val minHeight: Long? = when (tag) { "latest" -> head.getCurrentHeight() - // for earliest it doesn't nothing, we expect to have 0 block - "earliest" -> 0L + "earliest" -> 0L // for earliest it doesn't nothing, we expect to have 0 block else -> if (tag.startsWith("0x")) { return if (tag.length == 66) { // 32-byte hash is represented as 0x + 64 characters blockByHash(tag, head) } else { blockByHeight(tag) } - } else if (tag.startsWith("{") && list[pos] is Map<*, *>) { - // see https://eips.ethereum.org/EIPS/eip-1898 - val obj = list[pos] as Map<*, *> - when { - obj.containsKey("blockNumber") -> { - return blockByHeight(obj["blockNumber"].toString()) - } - obj.containsKey("blockHash") -> { - return blockByHash(obj["blockHash"].toString(), head) - } - else -> null - } } else { log.debug("Invalid tag: $tag") null @@ -128,8 +146,7 @@ class EthereumCallSelector( Mono.empty() } } - - private fun blockMethodSelector(method: String, params: String): Mono { + private fun blockMethodSelector(method: String, params: String, head: Head): Mono { val list = objectMapper.readerFor(Any::class.java).readValues(params).readAll() if (list.isEmpty()) { return Mono.empty() @@ -138,7 +155,7 @@ class EthereumCallSelector( return when (method) { "eth_getTransactionByBlockHashAndIndex", "eth_getBlockByHash" -> blockByHashFromCache(hashOrNumber) - "eth_getTransactionByBlockNumberAndIndex", "eth_getBlockByNumber" -> blockByHeight(hashOrNumber) + "eth_getTransactionByBlockNumberAndIndex", "eth_getBlockByNumber" -> blockSelectorByTag(hashOrNumber, head) else -> Mono.empty() } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelectorSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelectorSpec.groovy index ca0e360c..fcf585f6 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelectorSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelectorSpec.groovy @@ -264,22 +264,29 @@ class EthereumCallSelectorSpec extends Specification { setup: def cache = Stub(Caches) def callSelector = new EthereumCallSelector(Stub(Reader), cache) - def head = Stub(Head) + def head = Mock(Head) { + _ * getCurrentHeight() >> 17654321L + } when: def act = callSelector.getMatcher( - method, '["0xfbfe3b", false]', - head, false + method, params, head, false ) then: StepVerifier.create(act) - .expectNext(new Selector.HeightMatcher(16514619L)) + .expectNext(new Selector.HeightMatcher(height)) .expectComplete() .verify(Duration.ofSeconds(1)) where: - method << ["eth_getTransactionByBlockNumberAndIndex", "eth_getBlockByNumber"] + method | params | height + "eth_getTransactionByBlockNumberAndIndex" | '["0xfbfe3b", false]' | 16514619L + "eth_getTransactionByBlockNumberAndIndex" | '["earliest", false]' | 0L + "eth_getTransactionByBlockNumberAndIndex" | '["latest", false]' | 17654321L + "eth_getBlockByNumber" | '["0xfbfe3b", false]' | 16514619L + "eth_getBlockByNumber" | '["earliest", false]' | 0L + "eth_getBlockByNumber" | '["latest", false]' | 17654321L } def "No height matcher for getByHash method"() { @@ -307,4 +314,28 @@ class EthereumCallSelectorSpec extends Specification { where: resultFromCache << [Mono.empty(), Mono.error(new RuntimeException())] } + + def "Get height matcher for getLogs method"() { + setup: + def cache = Stub(Caches) + def callSelector = new EthereumCallSelector(Stub(Reader), cache) + def head = Mock(Head) { + _ * getCurrentHeight() >> 17654321L + } + + when: + def act = callSelector.getMatcher(method, param, head, false) + + then: + StepVerifier.create(act) + .expectNext(new Selector.HeightMatcher(height)) + .expectComplete() + .verify(Duration.ofSeconds(1)) + + where: + method | param | height + "eth_getLogs" | '[{"toBlock":"0xfbfe3b"}]' | 16514619L + "eth_getLogs" | '[{"toBlock":"latest"}]' | 17654321L + "eth_getLogs" | '[{"toBlock":"earliest"}]' | 0L + } }