changes from upstream: removed RpcReader and use better flow for tx receipt

This commit is contained in:
Termina1
2022-12-09 23:04:50 +02:00
parent c41d8c85ba
commit a4f0d44105
19 changed files with 139 additions and 372 deletions

View File

@@ -262,6 +262,6 @@ class CachesSpec extends Specification {
then:
1 * head.getCurrentHeight() >> 0xccf6e2
1 * receiptMemCache.acceptsRecentBlocks(0) >> true
1 * receiptMemCache.add(receiptContainer)
1 * receiptMemCache.add(receiptContainer) >> Mono.empty().then()
}
}

View File

@@ -29,9 +29,10 @@ import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosRpcUpstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import org.jetbrains.annotations.NotNull
class MultistreamHolderMock implements MultistreamHolder {
@@ -87,7 +88,7 @@ class MultistreamHolderMock implements MultistreamHolder {
static class EthereumMultistreamMock extends EthereumPosMultiStream {
EthereumReader customReader = null
EthereumCachingReader customReader = null
CallMethods customMethods = null
Head customHead = null
@@ -104,7 +105,7 @@ class MultistreamHolderMock implements MultistreamHolder {
}
@Override
EthereumReader getReader() {
EthereumCachingReader getReader() {
if (customReader != null) {
return customReader
}

View File

@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CurrentBlockCache
import io.emeraldpay.dshackle.data.DefaultContainer
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
import io.emeraldpay.dshackle.quorum.QuorumRpcReader
import io.emeraldpay.dshackle.reader.Reader
@@ -12,6 +13,7 @@ import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.etherjar.rpc.json.TransactionReceiptJson
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
@@ -22,7 +24,6 @@ import org.apache.commons.collections4.Factory
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
import java.time.Instant
@@ -171,6 +172,76 @@ class EthereumDirectReaderSpec extends Specification {
.verify(Duration.ofSeconds(1))
}
def "Reads tx receipt"() {
setup:
def json = new TransactionReceiptJson().tap {
transactionHash = TransactionId.from(hash1)
blockNumber = 100
blockHash = BlockHash.from(hash1)
}
def up = Mock(Multistream) {
1 * getApiSource(_) >> Stub(ApiSource)
}
def calls = Mock(Factory) {
1 * create() >> new DefaultEthereumMethods(Chain.ETHEREUM)
}
EthereumDirectReader reader = new EthereumDirectReader(
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, new ArrayList<Byte>()
)
)
}
}
when:
def act = reader.receiptReader.read(TransactionId.from(hash1))
.block(Duration.ofSeconds(1))
.with { new String(it) }
then:
act == '{"blockHash":"0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5","blockNumber":"0x64","transactionHash":"0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5","logs":[]}'
}
def "Puts tx receipt in cache after reading"() {
setup:
def json = new TransactionReceiptJson().tap {
transactionHash = TransactionId.from(hash1)
blockNumber = 100
blockHash = BlockHash.from(hash1)
}
def up = Mock(Multistream) {
1 * getApiSource(_) >> Stub(ApiSource)
}
def calls = Mock(Factory) {
1 * create() >> new DefaultEthereumMethods(Chain.ETHEREUM)
}
def caches = Mock(Caches) {
// note that the Caches needs a Height value, otherwise it's not cached
1 * cacheReceipt(Caches.Tag.REQUESTED, { DefaultContainer data -> data.txId.toHex() == hash1.substring(2) && data.height == 100 })
}
EthereumDirectReader reader = new EthereumDirectReader(
up, caches, new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, new ArrayList<Byte>()
)
)
}
}
when:
def act = reader.receiptReader.read(TransactionId.from(hash1))
.block(Duration.ofSeconds(1))
.with { new String(it) }
then:
act == '{"blockHash":"0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5","blockNumber":"0x64","transactionHash":"0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5","logs":[]}'
}
def "Produce empty on non-existing tx"() {
setup:
def up = Mock(Multistream) {

View File

@@ -30,7 +30,7 @@ class EthereumLegacyFeesSpec extends Specification {
it.gasPrice = Wei.ofUnits(8, Wei.Unit.GWEI)
}
def fees = new EthereumLegacyFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
def fees = new EthereumLegacyFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = fees.extractFee(block, tx)
then:

View File

@@ -44,7 +44,7 @@ class EthereumPriorityFeesSpec extends Specification {
it.maxPriorityFeePerGas = Wei.ofUnits(5.0001, Wei.Unit.GWEI)
}
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = fees.extractFee(block, tx)
then:
@@ -66,7 +66,7 @@ class EthereumPriorityFeesSpec extends Specification {
it.gasPrice = Wei.from("0x198286458f")
}
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = fees.extractFee(block, tx)
then:
@@ -83,7 +83,7 @@ class EthereumPriorityFeesSpec extends Specification {
new EthereumFees.EthereumFee(Wei.ofEthers(0.75), Wei.ofEthers(0.5), Wei.ofEthers(0.75), Wei.ofEthers(0.5)),
new EthereumFees.EthereumFee(Wei.ofEthers(0.6), Wei.ofEthers(0.2), Wei.ofEthers(0.6), Wei.ofEthers(0.5)),
]
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumReader), 10)
def fees = new EthereumPriorityFees(Stub(EthereumMultistream), Stub(EthereumCachingReader), 10)
when:
def act = Flux.fromIterable(inputs)
.transform(fees.feeAggregation(ChainFees.Mode.AVG_LAST))
@@ -127,7 +127,7 @@ class EthereumPriorityFeesSpec extends Specification {
1 * getCurrentHeight() >> 13756007
}
}
def reader = Mock(EthereumReader) {
def reader = Mock(EthereumCachingReader) {
_ * it.blocksByHeightParsed() >> Mock(Reader) {
1 * it.read(13756006) >> Mono.just(block1)
1 * it.read(13756007) >> Mono.just(block2)

View File

@@ -1,281 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.cache.BlocksMemCache
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.ReceiptRedisCache
import io.emeraldpay.dshackle.cache.TxMemCache
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxContainer
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.test.EthereumPosRpcUpstreamMock
import io.emeraldpay.dshackle.test.EthereumRpcUpstreamMock
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.apache.commons.collections4.Factory
import org.apache.commons.collections4.functors.ConstantFactory
import reactor.core.publisher.Mono
import spock.lang.Specification
import java.time.Instant
class EthereumReaderSpec extends Specification {
def blockId = BlockId.from("f85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2")
def blockJson = new BlockJson<TransactionRefJson>().tap { blockJson ->
blockJson.hash = BlockHash.from(blockId.value)
blockJson.totalDifficulty = BigInteger.ONE
blockJson.number = 101
blockJson.timestamp = Instant.ofEpochSecond(100000000)
blockJson.transactions = []
blockJson.uncles = []
}
def txId = BlockId.from("a38e7b4d456777c94b46c61a1e4cf52fbdd92acc4444719d1fad77005698c221")
def txJson = new TransactionJson().tap { json ->
json.hash = TransactionId.from(txId.value)
json.blockHash = blockJson.hash
json.blockNumber = blockJson.number
}
Factory<CallMethods> calls = ConstantFactory.constantFactory(new DefaultEthereumMethods(Chain.ETHEREUM))
def "Block by Id reads from cache"() {
setup:
def memCache = Mock(BlocksMemCache) {
1 * read(blockId) >> Mono.just(BlockContainer.from(blockJson))
}
def caches = Caches.newBuilder()
.setBlockByHash(memCache)
.build()
def reader = new EthereumReader(Stub(Multistream), caches, calls)
when:
def act = reader.blocksByIdParsed().read(blockId).block()
then:
act == blockJson
}
def "Block by Id reads from api if cache is empty"() {
setup:
def memCache = Mock(BlocksMemCache) {
1 * read(blockId) >> Mono.empty()
}
def caches = Caches.newBuilder()
.setBlockByHash(memCache)
.build()
def api = TestingCommons.api()
api.answer("eth_getBlockByHash", ["0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2", false], blockJson)
def upstream = TestingCommons.multistream(api)
def reader = new EthereumReader(upstream, caches, calls)
when:
def act = reader.blocksByIdParsed().read(blockId).block()
then:
act == blockJson
}
def "Block by Id reads from api if cache failed"() {
setup:
def memCache = Mock(BlocksMemCache) {
1 * read(blockId) >> Mono.error(new IllegalStateException("Test error"))
}
def caches = Caches.newBuilder()
.setBlockByHash(memCache)
.build()
def api = TestingCommons.api()
api.answer("eth_getBlockByHash", ["0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2", false], blockJson)
def upstream = TestingCommons.multistream(api)
def reader = new EthereumReader(upstream, caches, calls)
when:
def act = reader.blocksByIdParsed().read(blockId).block()
then:
act == blockJson
}
def "Block by Hash reads from cache"() {
setup:
def memCache = Mock(BlocksMemCache) {
1 * read(blockId) >> Mono.just(BlockContainer.from(blockJson))
}
def caches = Caches.newBuilder()
.setBlockByHash(memCache)
.build()
def reader = new EthereumReader(Stub(Multistream), caches, calls)
when:
def act = reader.blocksByHashParsed().read(blockJson.hash).block()
then:
act == blockJson
}
def "Block by Hash reads from api if cache is empty"() {
setup:
def memCache = Mock(BlocksMemCache) {
1 * read(blockId) >> Mono.empty()
}
def caches = Caches.newBuilder()
.setBlockByHash(memCache)
.build()
def api = TestingCommons.api()
api.answer("eth_getBlockByHash", ["0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2", false], blockJson)
def upstream = TestingCommons.multistream(api)
def reader = new EthereumReader(upstream, caches, calls)
when:
def act = reader.blocksByHashParsed().read(blockJson.hash).block()
then:
act == blockJson
}
def "Tx by Hash reads from cache"() {
setup:
def memCache = Mock(TxMemCache) {
1 * read(txId) >> Mono.just(TxContainer.from(txJson))
}
def caches = Caches.newBuilder()
.setTxByHash(memCache)
.build()
def reader = new EthereumReader(Stub(Multistream), caches, calls)
when:
def act = reader.txByHash().read(txJson.hash).block()
then:
act == txJson
}
def "Tx by Hash reads from api if cache is empty"() {
setup:
def memCache = Mock(TxMemCache) {
1 * read(txId) >> Mono.empty()
}
def caches = Caches.newBuilder()
.setTxByHash(memCache)
.build()
def api = TestingCommons.api()
api.answer("eth_getTransactionByHash", [txJson.hash.toHex()], txJson)
def upstream = TestingCommons.multistream(api)
def reader = new EthereumReader(upstream, caches, calls)
when:
def act = reader.txByHash().read(txJson.hash).block()
then:
act == txJson
}
def "Caches balance until block mined"() {
setup:
def api = TestingCommons.api()
// no height
api.answerOnce("eth_getBalance", ["0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c", "latest"], "0x10")
// height 101 + 1 => 102 => 0x66
api.answerOnce("eth_getBalance", ["0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c", "0x66"], "0xff")
EthereumPosRpcUpstreamMock upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM, api)
def upstreams = TestingCommons.multistream(upstream)
def reader = new EthereumReader(upstreams, Caches.default(), calls)
reader.start()
when:
def act = reader.balance().read(Address.from("0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c")).block()
then:
act == Wei.from("0x10")
when:
//now it should use cached value, without actual request
act = reader.balance().read(Address.from("0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c")).block()
then:
act == Wei.from("0x10")
when:
//move head forward, which should erase cache
def block2 = blockJson.copy().tap {
it.number++
it.totalDifficulty = BigInteger.TWO
}
upstream.nextBlock(BlockContainer.from(block2))
Thread.sleep(50)
act = reader.balance().read(Address.from("0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c")).block()
then:
act == Wei.from("0xff")
}
def "Read receipt from upstream if cache is empty"() {
setup:
def api = TestingCommons.api()
api.answerOnce("eth_getTransactionReceipt", ["0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2"], [
transactionHash: "0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2"
])
EthereumPosRpcUpstreamMock upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM, api)
def upstreams = TestingCommons.multistream(upstream)
def reader = new EthereumReader(upstreams, Caches.default(), calls)
reader.start()
when:
def act = reader.receipts().read(TxId.from("0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2")).block()
then:
act != null
new String(act) == '{"transactionHash":"0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2"}'
}
def "Read receipt from cache if available"() {
setup:
def api = TestingCommons.api()
EthereumPosRpcUpstreamMock upstream = new EthereumPosRpcUpstreamMock(Chain.ETHEREUM, api)
def upstreams = TestingCommons.multistream(upstream)
def receiptCache = Mock(ReceiptRedisCache) {
1 * it.read(TxId.from("0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2")) >>
Mono.just('{"transactionHash":"0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2"}'.bytes)
}
def cashes = Caches.newBuilder()
.setReceipts(receiptCache)
.build()
def reader = new EthereumReader(upstreams, cashes, calls)
reader.start()
when:
def act = reader.receipts().read(TxId.from("0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2")).block()
then:
act != null
new String(act) == '{"transactionHash":"0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2"}'
api.calls.get() == 0
}
}

View File

@@ -23,7 +23,7 @@ class LocalCallRouterSpec extends Specification {
setup:
def methods = new DefaultEthereumMethods(Chain.ETHEREUM)
def router = new LocalCallRouter(
new EthereumReader(
new EthereumCachingReader(
TestingCommons.multistream(TestingCommons.api()),
Caches.default(),
ConstantFactory.constantFactory(new DefaultEthereumMethods(Chain.ETHEREUM))
@@ -41,7 +41,7 @@ class LocalCallRouterSpec extends Specification {
setup:
def methods = new DefaultEthereumMethods(Chain.ETHEREUM)
def router = new LocalCallRouter(
new EthereumReader(
new EthereumCachingReader(
TestingCommons.multistream(TestingCommons.api()),
Caches.default(),
ConstantFactory.constantFactory(new DefaultEthereumMethods(Chain.ETHEREUM))
@@ -61,7 +61,7 @@ class LocalCallRouterSpec extends Specification {
def head = Mock(Head) {
1 * getCurrentHeight() >> 101L
}
def reader = Mock(EthereumReader) {
def reader = Mock(EthereumCachingReader) {
_ * blocksByIdAsCont() >> new EmptyReader<>()
_ * txByHashAsCont() >> new EmptyReader<>()
1 * blocksByHeightAsCont() >> Mock(Reader) {
@@ -87,7 +87,7 @@ class LocalCallRouterSpec extends Specification {
def "getBlockByNumber with earliest uses 0 block"() {
setup:
def head = Stub(Head) {}
def reader = Mock(EthereumReader) {
def reader = Mock(EthereumCachingReader) {
_ * blocksByIdAsCont() >> new EmptyReader<>()
_ * txByHashAsCont() >> new EmptyReader<>()
1 * blocksByHeightAsCont() >> Mock(Reader) {
@@ -113,7 +113,7 @@ class LocalCallRouterSpec extends Specification {
def "getBlockByNumber fetches the block"() {
setup:
def head = Stub(Head) {}
def reader = Mock(EthereumReader) {
def reader = Mock(EthereumCachingReader) {
_ * blocksByIdAsCont() >> new EmptyReader<>()
_ * txByHashAsCont() >> new EmptyReader<>()
1 * blocksByHeightAsCont() >> Mock(Reader) {