From d83e2f1c6b7bad1b13f47e6d4259914f7e4d646d Mon Sep 17 00:00:00 2001 From: KirillPamPam Date: Tue, 30 May 2023 17:28:26 +0400 Subject: [PATCH] Add height matcher for some methods (#218) --- .../io/emeraldpay/dshackle/rpc/NativeCall.kt | 3 +- .../upstream/calls/EthereumCallSelector.kt | 84 ++++++------ .../calls/EthereumCallSelectorSpec.groovy | 129 +++++++++++------- 3 files changed, 119 insertions(+), 97 deletions(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt index 79d00444..cbc46e09 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt @@ -95,10 +95,9 @@ open class NativeCall( fun onUpstreamChangeEvent(event: UpstreamChangeEvent) { casting[BlockchainType.from(event.chain)]?.let { cast -> multistreamHolder.getUpstream(event.chain).let { up -> - val reader = up.cast(cast).getReader() ethereumCallSelectors.putIfAbsent( event.chain, - EthereumCallSelector(reader.heightByHash(), up.caches) + EthereumCallSelector(up.caches) ) } } 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 146bcdcf..dba0b9a4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelector.kt @@ -18,14 +18,12 @@ package io.emeraldpay.dshackle.upstream.calls import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.data.BlockId -import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.etherjar.hex.HexQuantity import org.bouncycastle.util.encoders.DecoderException import org.slf4j.LoggerFactory import reactor.core.publisher.Mono -import java.util.Collections import java.util.Objects /** @@ -33,7 +31,6 @@ import java.util.Objects * The implementation is specific for Ethereum. */ class EthereumCallSelector( - private val heightReader: Reader, private val caches: Caches ) { @@ -41,17 +38,25 @@ class EthereumCallSelector( private val log = LoggerFactory.getLogger(EthereumCallSelector::class.java) // ref https://eth.wiki/json-rpc/API#the-default-block-parameter - private val TAG_METHODS = listOf( + private val TAG_METHODS = setOf( "eth_getBalance", "eth_getCode", "eth_getTransactionCount", // no "eth_getStorageAt" because it has different structure, and therefore separate logic - "eth_call" - ).sorted() + "eth_call", + "eth_getRootHash", + "bor_getRootHash" + ) + + private val FILTER_OBJECT_METHODS = setOf( + "eth_newFilter", "eth_getLogs" + ) private val GET_BY_HASH_OR_NUMBER_METHODS = setOf( - "eth_getBlockByHash", "eth_getBlockByNumber", - "eth_getTransactionByBlockHashAndIndex", "eth_getTransactionByBlockNumberAndIndex" + "eth_getBlockByHash", "eth_getBlockByNumber", "bor_getSignersAtHash", + "eth_getTransactionByBlockHashAndIndex", "eth_getTransactionByBlockNumberAndIndex", + "eth_getBlockTransactionCountByNumber", "bor_getAuthor", "eth_getUncleCountByBlockNumber", + "eth_getUncleByBlockNumberAndIndex" ) } @@ -65,14 +70,19 @@ class EthereumCallSelector( if (method in DefaultEthereumMethods.withFilterIdMethods) { return sameUpstreamMatcher(params) } else if (!passthrough) { // passthrough indicates we should match only labels - if (Collections.binarySearch(TAG_METHODS, method) >= 0) { - return blockTagSelector(params, 1, null, head) - } else if (method == "eth_getStorageAt") { - return blockTagSelector(params, 2, null, head) - } else if (method in GET_BY_HASH_OR_NUMBER_METHODS) { - return blockMethodSelector(method, params, head) - } else if (method == "eth_getLogs") { - return blockTagSelector(params, 0, "toBlock", head) + when (method) { + in TAG_METHODS -> { + return blockTagSelector(params, 1, null, head) + } + "eth_getStorageAt" -> { + return blockTagSelector(params, 2, null, head) + } + in GET_BY_HASH_OR_NUMBER_METHODS -> { + return blockTagSelector(params, 0, null, head) + } + in FILTER_OBJECT_METHODS -> { + return blockTagSelector(params, 0, "toBlock", head) + } } } return Mono.empty() @@ -91,6 +101,7 @@ class EthereumCallSelector( val nodeId = hashHex.toInt(16) return Mono.just(Selector.SameNodeMatcher(nodeId.toByte())) } + 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) { @@ -131,9 +142,9 @@ class EthereumCallSelector( val minHeight: Long? = when (tag) { "latest" -> head.getCurrentHeight() "earliest" -> 0L // for earliest it doesn't nothing, we expect to have 0 block - else -> if (tag.startsWith("0x")) { + else -> if (tag.startsWith("0x") || tag.toLongOrNull() != null) { return if (tag.length == 66) { // 32-byte hash is represented as 0x + 64 characters - blockByHash(tag, head) + blockByHash(tag) } else { blockByHeight(tag) } @@ -148,21 +159,8 @@ class EthereumCallSelector( Mono.empty() } } - 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() - } - val hashOrNumber = Objects.toString(list[0]) - return when (method) { - "eth_getTransactionByBlockHashAndIndex", "eth_getBlockByHash" -> blockByHashFromCache(hashOrNumber) - "eth_getTransactionByBlockNumberAndIndex", "eth_getBlockByNumber" -> blockSelectorByTag(hashOrNumber, head) - else -> Mono.empty() - } - } - - private fun blockByHashFromCache(blockHash: String): Mono { + private fun blockByHash(blockHash: String): Mono { return try { caches.getLastHeightByHash() .read(BlockId.from(blockHash)) @@ -176,22 +174,16 @@ class EthereumCallSelector( private fun blockByHeight(blockNumber: String): Mono { return try { - Mono.just(Selector.HeightMatcher(HexQuantity.from(blockNumber).value.longValueExact())) + val height = + if (blockNumber.startsWith("0x")) { + HexQuantity.from(blockNumber).value.longValueExact() + } else { + blockNumber.toLong() + } + Mono.just(Selector.HeightMatcher(height)) } catch (t: Throwable) { log.warn("Invalid blockNumber: $blockNumber") - Mono.empty() - } - } - - private fun blockByHash(blockHash: String, head: Head): Mono { - return try { - val blockId = BlockId.from(blockHash) - heightReader.read(blockId) - .switchIfEmpty(Mono.justOrEmpty(head.getCurrentHeight())) - .map { Selector.HeightMatcher(it) } - } catch (t: Throwable) { - log.warn("Invalid blockHash: $blockHash") - Mono.empty() + 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 c9c4cfb6..cc86ff57 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelectorSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/EthereumCallSelectorSpec.groovy @@ -15,11 +15,9 @@ */ package io.emeraldpay.dshackle.upstream.calls - import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.HeightByHashMemCache import io.emeraldpay.dshackle.data.BlockId -import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Selector import reactor.core.publisher.Mono @@ -32,7 +30,7 @@ class EthereumCallSelectorSpec extends Specification { def "Get height matcher for latest balance"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { 1 * getCurrentHeight() >> 100 } @@ -44,7 +42,7 @@ class EthereumCallSelectorSpec extends Specification { def "Get height matcher for latest call"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { 1 * getCurrentHeight() >> 100 } @@ -56,7 +54,7 @@ class EthereumCallSelectorSpec extends Specification { def "Get height matcher for latest storageAt"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { 1 * getCurrentHeight() >> 100 } @@ -68,7 +66,7 @@ class EthereumCallSelectorSpec extends Specification { def "Get height matcher for balance on block"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { _ * getCurrentHeight() >> 100 } @@ -80,10 +78,12 @@ class EthereumCallSelectorSpec extends Specification { def "Get height matcher for balance on block referred by hash"() { setup: - def heights = Mock(Reader) { - 1 * it.read(BlockId.from("0xc90f1c8c125a4d5b90742f16947bdb1d10516f173fd7fc51223d10499de2a812")) >> Mono.just(8606722L) + def cache = Mock(Caches) { caches -> + 1 * caches.getLastHeightByHash() >> Mock(HeightByHashMemCache) { memCache -> + 1 * memCache.read(BlockId.from("0xc90f1c8c125a4d5b90742f16947bdb1d10516f173fd7fc51223d10499de2a812")) >> Mono.just(8606722L) + } } - EthereumCallSelector callSelector = new EthereumCallSelector(heights, Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(cache) def head = Mock(Head) { _ * getCurrentHeight() >> 9128116 } @@ -95,7 +95,7 @@ class EthereumCallSelectorSpec extends Specification { def "No matcher for invalid height"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { _ * getCurrentHeight() >> 100 } @@ -108,7 +108,7 @@ class EthereumCallSelectorSpec extends Specification { def "No matcher for negative height"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { _ * getCurrentHeight() >> 100 } @@ -120,7 +120,7 @@ class EthereumCallSelectorSpec extends Specification { def "No matcher for negative long"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { _ * getCurrentHeight() >> 100 } @@ -133,7 +133,7 @@ class EthereumCallSelectorSpec extends Specification { def "No matcher for pending balance"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) { _ * getCurrentHeight() >> 100 } @@ -145,7 +145,7 @@ class EthereumCallSelectorSpec extends Specification { def "Get height matcher with EIP-1898"() { setup: - EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Reader), Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(Stub(Caches)) def head = Stub(Head) when: def act = callSelector.getMatcher("eth_call", '["0x0000", {"blockNumber": "0x100"}]', head, false).block() @@ -155,10 +155,12 @@ class EthereumCallSelectorSpec extends Specification { def "Get hash matcher with EIP-1898"() { setup: - def heights = Mock(Reader) { - 1 * it.read(BlockId.from("0xa6af163aab691919c595e2a466f0a7b01f1dff8cfd9631dee811df57064c2d32")) >> Mono.just(12079192L) + def cache = Mock(Caches) { caches -> + 1 * caches.getLastHeightByHash() >> Mock(HeightByHashMemCache) { memCache -> + 1 * memCache.read(BlockId.from("0xa6af163aab691919c595e2a466f0a7b01f1dff8cfd9631dee811df57064c2d32")) >> Mono.just(12079192L) + } } - EthereumCallSelector callSelector = new EthereumCallSelector(heights, Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(cache) def head = Stub(Head) when: def act = callSelector.getMatcher("eth_call", @@ -170,10 +172,12 @@ class EthereumCallSelectorSpec extends Specification { def "Get empty matcher for block tag with passthrough arg"() { setup: - def heights = Mock(Reader) { - 0 * it.read(BlockId.from("0xa6af163aab691919c595e2a466f0a7b01f1dff8cfd9631dee811df57064c2d32")) >> Mono.just(12079192L) + def cache = Mock(Caches) { caches -> + 0 * caches.getLastHeightByHash() >> Mock(HeightByHashMemCache) { memCache -> + 0 * memCache.read(BlockId.from("0xa6af163aab691919c595e2a466f0a7b01f1dff8cfd9631dee811df57064c2d32")) >> Mono.just(12079192L) + } } - EthereumCallSelector callSelector = new EthereumCallSelector(heights, Stub(Caches)) + EthereumCallSelector callSelector = new EthereumCallSelector(cache) def head = Stub(Head) when: def act = callSelector.getMatcher("eth_call", @@ -183,26 +187,9 @@ class EthereumCallSelectorSpec extends Specification { act == null } - def "Match head if hash matcher for unknown hash"() { - setup: - def heights = Mock(Reader) { - 1 * it.read(BlockId.from("0xa6af163aab691919c595e2a466f0a7b01f1dff8cfd9631dee811df57064c2d32")) >> Mono.empty() - } - EthereumCallSelector callSelector = new EthereumCallSelector(heights, Stub(Caches)) - def head = Mock(Head) { - 1 * it.getCurrentHeight() >> 100 - } - when: - def act = callSelector.getMatcher("eth_call", - '["0x0000", {"blockHash": "0xa6af163aab691919c595e2a466f0a7b01f1dff8cfd9631dee811df57064c2d32"}]', head, false) - .block() - then: - act == new Selector.HeightMatcher(100) - } - def "Get same matcher for getFilterChanges method"() { setup: - def callSelector = new EthereumCallSelector(Mock(Reader), Stub(Caches)) + def callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) expect: @@ -219,7 +206,7 @@ class EthereumCallSelectorSpec extends Specification { def "Get empty matcher for getFilterChanges method without params"() { setup: - def callSelector = new EthereumCallSelector(Mock(Reader), Stub(Caches)) + def callSelector = new EthereumCallSelector(Stub(Caches)) def head = Mock(Head) when: @@ -238,7 +225,7 @@ class EthereumCallSelectorSpec extends Specification { 1 * memCache.read(BlockId.from(hash)) >> Mono.just(blockHeight) } } - def callSelector = new EthereumCallSelector(Stub(Reader), cache) + def callSelector = new EthereumCallSelector(cache) def head = Stub(Head) when: @@ -254,13 +241,13 @@ class EthereumCallSelectorSpec extends Specification { .verify(Duration.ofSeconds(1)) where: - method << ["eth_getTransactionByBlockHashAndIndex", "eth_getBlockByHash"] + method << ["eth_getTransactionByBlockHashAndIndex", "eth_getBlockByHash", "bor_getSignersAtHash"] } def "No height matcher for getByNumber and getTransactionByBlockNumber methods when passthrough is on"() { setup: def cache = Stub(Caches) - def callSelector = new EthereumCallSelector(Stub(Reader), cache) + def callSelector = new EthereumCallSelector(cache) def head = Stub(Head) when: @@ -283,10 +270,11 @@ class EthereumCallSelectorSpec extends Specification { "eth_getBlockByNumber" | '["earliest", false]' "eth_getBlockByNumber" | '["latest", false]' } - def "Get height matcher for getByNumber and getTransactionByBlockNumber methods"() { + + def "Get height matcher for byNumber methods"() { setup: def cache = Stub(Caches) - def callSelector = new EthereumCallSelector(Stub(Reader), cache) + def callSelector = new EthereumCallSelector(cache) def head = Mock(Head) { _ * getCurrentHeight() >> 17654321L } @@ -310,6 +298,18 @@ class EthereumCallSelectorSpec extends Specification { "eth_getBlockByNumber" | '["0xfbfe3b", false]' | 16514619L "eth_getBlockByNumber" | '["earliest", false]' | 0L "eth_getBlockByNumber" | '["latest", false]' | 17654321L + "eth_getBlockTransactionCountByNumber" | '["latest"]' | 17654321L + "eth_getBlockTransactionCountByNumber" | '["0xfbfe3b"]' | 16514619L + "eth_getBlockTransactionCountByNumber" | '["earliest"]' | 0L + "eth_getUncleCountByBlockNumber" | '["latest"]' | 17654321L + "eth_getUncleCountByBlockNumber" | '["0xfbfe3b"]' | 16514619L + "eth_getUncleCountByBlockNumber" | '["earliest"]' | 0L + "eth_getUncleCountByBlockNumber" | '["latest"]' | 17654321L + "eth_getUncleCountByBlockNumber" | '["0xfbfe3b"]' | 16514619L + "eth_getUncleCountByBlockNumber" | '["earliest"]' | 0L + "eth_getUncleByBlockNumberAndIndex" | '["latest"]' | 17654321L + "eth_getUncleByBlockNumberAndIndex" | '["0xfbfe3b"]' | 16514619L + "eth_getUncleByBlockNumberAndIndex" | '["earliest"]' | 0L } def "No height matcher for getByHash method"() { @@ -320,7 +320,7 @@ class EthereumCallSelectorSpec extends Specification { 1 * memCache.read(BlockId.from(hash)) >> resultFromCache } } - def callSelector = new EthereumCallSelector(Stub(Reader), cache) + def callSelector = new EthereumCallSelector(cache) def head = Stub(Head) when: @@ -339,10 +339,10 @@ class EthereumCallSelectorSpec extends Specification { resultFromCache << [Mono.empty(), Mono.error(new RuntimeException())] } - def "Get height matcher for getLogs method"() { + def "Get height matcher for getLogs and eth_newFilter method"() { setup: def cache = Stub(Caches) - def callSelector = new EthereumCallSelector(Stub(Reader), cache) + def callSelector = new EthereumCallSelector(cache) def head = Mock(Head) { _ * getCurrentHeight() >> 17654321L } @@ -361,12 +361,15 @@ class EthereumCallSelectorSpec extends Specification { "eth_getLogs" | '[{"toBlock":"0xfbfe3b"}]' | 16514619L "eth_getLogs" | '[{"toBlock":"latest"}]' | 17654321L "eth_getLogs" | '[{"toBlock":"earliest"}]' | 0L + "eth_newFilter" | '[{"toBlock":"0xfbfe2b", "toBlock":"0xfbfe3b"}]' | 16514619L + "eth_newFilter" | '[{"toBlock":"0xfbfe3b", "toBlock":"latest"}]' | 17654321L + "eth_newFilter" | '[{"toBlock":"0xfbfe3b", "toBlock":"earliest"}]' | 0L } - def "No height matcher for getLogs method when passthrough is on"() { + def "No height matcher for getLogs and eth_newFilter method when passthrough is on"() { setup: def cache = Stub(Caches) - def callSelector = new EthereumCallSelector(Stub(Reader), cache) + def callSelector = new EthereumCallSelector(cache) def head = Stub(Head) when: @@ -383,5 +386,33 @@ class EthereumCallSelectorSpec extends Specification { "eth_getLogs" | '[{"toBlock":"0xfbfe3b"}]' "eth_getLogs" | '[{"toBlock":"latest"}]' "eth_getLogs" | '[{"toBlock":"earliest"}]' + "eth_newFilter" | '[{"toBlock":"0xfbfe2b", "toBlock":"0xfbfe3b"}]' + "eth_newFilter" | '[{"toBlock":"0xfbfe3b", "toBlock":"latest"}]' + "eth_newFilter" | '[{"toBlock":"0xfbfe3b", "toBlock":"earliest"}]' + } + + def "Get height matcher for bor method"() { + setup: + def cache = Stub(Caches) + def callSelector = new EthereumCallSelector(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_getRootHash" | '[1023, 1200]' | 1200L + "bor_getRootHash" | '[1000, 1400]' | 1400L + "bor_getAuthor" | '["latest"]' | 17654321L + "bor_getAuthor" | '["0xfbfe3b"]' | 16514619L } }