diff --git a/build.gradle b/build.gradle index 239c1df2..2ec4366d 100644 --- a/build.gradle +++ b/build.gradle @@ -81,7 +81,6 @@ dependencies { implementation "io.infinitape:etherjar-hex:$etherjarVersion" implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion" implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion" - implementation "io.infinitape:etherjar-rpc-emerald:$etherjarVersion" implementation "io.infinitape:etherjar-tx:$etherjarVersion" implementation 'org.bitcoinj:bitcoinj-core:0.15.8' @@ -125,7 +124,7 @@ compileTestKotlin { test { jvmArgs '-ea' - testLogging.showStandardStreams = true + testLogging.showStandardStreams = false testLogging.exceptionFormat = 'full' } @@ -199,4 +198,38 @@ task generateVersion() { ].join("\n") } +} + +// Show the list of failed tests and output only for them, helpful for CI +ext.failedTests = [] +tasks.withType(Test) { + def stdout = new LinkedList() + beforeTest { TestDescriptor td -> + stdout.clear() + } + onOutput { TestDescriptor td, TestOutputEvent toe -> + stdout.addAll(toe.getMessage().split('(?m)$')) + while (stdout.size() > 100) { + stdout.remove() + } + } + afterTest { TestDescriptor descriptor, TestResult result -> + if(result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.FAILURE){ + failedTests << "${descriptor.className} > ${descriptor.name}" + if (!stdout.isEmpty()) { + println("-------- ${descriptor.className} > ${descriptor.name} OUTPUT ".padRight(120, "-")) + stdout.each { print(it) } + println("================".padRight(120, "=")) + } + } + } +} +gradle.buildFinished { + if(!failedTests.empty){ + println "Failed tests for ${project.name}:" + failedTests.each { failedTest -> + println failedTest + } + println "" + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/cache/BlocksRedisCache.kt b/src/main/kotlin/io/emeraldpay/dshackle/cache/BlocksRedisCache.kt index d52bc9ff..a584fed3 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/cache/BlocksRedisCache.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/cache/BlocksRedisCache.kt @@ -93,6 +93,7 @@ class BlocksRedisCache( Instant.ofEpochMilli(meta.timestamp), false, value.value.toByteArray(), + null, meta.txHashesList.map { TxId(it.toByteArray()) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/cache/Caches.kt b/src/main/kotlin/io/emeraldpay/dshackle/cache/Caches.kt index fd84f48e..8b23723e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/cache/Caches.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/cache/Caches.kt @@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.data.TxContainer import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.reader.CompoundReader import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.ethereum.EthereumFullBlocksReader import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.TransactionJson import org.slf4j.LoggerFactory @@ -158,11 +159,11 @@ open class Caches( } fun getFullBlocks(): Reader { - return EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash) + return EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash) } fun getFullBlocksByHeight(): Reader { - return BlockByHeight(blocksByHeight, EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash)) + return BlockByHeight(blocksByHeight, EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash)) } enum class Tag { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/cache/TxRedisCache.kt b/src/main/kotlin/io/emeraldpay/dshackle/cache/TxRedisCache.kt index 817455e5..2d6f20fb 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/cache/TxRedisCache.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/cache/TxRedisCache.kt @@ -59,7 +59,10 @@ class TxRedisCache( fun toProto(value: TxContainer): ByteArray { val meta = CachesProto.TxMeta.newBuilder() .setHash(ByteString.copyFrom(value.hash.value)) - .setHeight(value.height) + + value.height?.let { + meta.setHeight(it) + } value.blockId?.value?.let { meta.setBlockHash(ByteString.copyFrom(it)) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index f4023592..fe0ab0a5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -86,19 +86,21 @@ class UpstreamsConfig { open class UpstreamConnection + open class RpcConnection : UpstreamConnection() { + var rpc: HttpEndpoint? = null + } + class GrpcConnection : UpstreamConnection() { var host: String? = null var port: Int = 0 var auth: AuthConfig.ClientTlsAuth? = null } - class EthereumConnection : UpstreamConnection() { - var rpc: HttpEndpoint? = null + class EthereumConnection : RpcConnection() { var ws: WsEndpoint? = null } - class BitcoinConnection : UpstreamConnection() { - var rpc: HttpEndpoint? = null + class BitcoinConnection : RpcConnection() { } class HttpEndpoint(val url: URI) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockContainer.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockContainer.kt index ddd00f35..61e42bcc 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockContainer.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockContainer.kt @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionRefJson +import org.apache.commons.codec.binary.Hex import java.math.BigInteger import java.time.Instant @@ -30,12 +31,13 @@ class BlockContainer( val timestamp: Instant, val full: Boolean, json: ByteArray?, + val parsed: Any?, val transactions: List = emptyList() -) : SourceContainer(json) { +) : SourceContainer(json, parsed) { companion object { @JvmStatic - fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer { + fun from(block: BlockJson<*>, raw: ByteArray): BlockContainer { val hasTransactions = block.transactions?.filterIsInstance()?.count() ?: 0 > 0 return BlockContainer( block.number, @@ -43,10 +45,26 @@ class BlockContainer( block.totalDifficulty, block.timestamp, hasTransactions, - objectMapper.writeValueAsBytes(block), + raw, + block, block.transactions?.map { TxId.from(it.hash) } ?: emptyList() ) } + + @JvmStatic + fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer { + return from(block, objectMapper.writeValueAsBytes(block)) + } + + @JvmStatic + fun from(raw: ByteArray, objectMapper: ObjectMapper): BlockContainer { + val block = objectMapper.readValue(raw, BlockJson::class.java) + return from(block, raw) + } + } + + override fun toString(): String { + return "Block $height = $hash" } override fun equals(other: Any?): Boolean { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt index f75551bf..c82432e9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt @@ -25,6 +25,11 @@ class BlockId( ) : HashId(value) { companion object { + @JvmStatic + fun from(hash: ByteArray): BlockId { + return BlockId(hash) + } + @JvmStatic fun from(hash: BlockHash): BlockId { return BlockId(hash.bytes) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt index 0fd1a73f..affe473c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt @@ -40,6 +40,10 @@ open class HashId( return String(hex) } + fun toHexWithPrefix(): String { + return "0x" + toHex() + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is HashId) return false diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/SourceContainer.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/SourceContainer.kt index 26e26a58..1dc82c61 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/SourceContainer.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/SourceContainer.kt @@ -16,10 +16,24 @@ */ package io.emeraldpay.dshackle.data +import java.lang.ClassCastException + abstract class SourceContainer( - val json: ByteArray? + val json: ByteArray?, + private val parsed: Any? ) { + fun getParsed(clazz: Class): T? { + if (parsed == null) { + return null + } + if (clazz.isAssignableFrom(parsed.javaClass)) { + return parsed as T + } + throw ClassCastException("Cannot cast ${parsed.javaClass} to $clazz") + } + + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SourceContainer) return false diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/TxContainer.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/TxContainer.kt index b7cf86b8..2e9ec38b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/TxContainer.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/TxContainer.kt @@ -20,20 +20,26 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.infinitape.etherjar.rpc.json.TransactionJson class TxContainer( - val height: Long, + val height: Long?, val hash: TxId, val blockId: BlockId?, - json: ByteArray? -) : SourceContainer(json) { + json: ByteArray?, + parsed: Any? = null +) : SourceContainer(json, parsed) { companion object { @JvmStatic fun from(tx: TransactionJson, objectMapper: ObjectMapper): TxContainer { + return from(tx, objectMapper.writeValueAsBytes(tx)) + } + + fun from(tx: TransactionJson, raw: ByteArray): TxContainer { return TxContainer( tx.blockNumber, TxId.from(tx.hash), - BlockId.from(tx.blockHash), - objectMapper.writeValueAsBytes(tx) + tx.blockHash?.let { BlockId.from(it) }, + raw, + tx ) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt index 1e3bc729..3887db3b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt @@ -32,13 +32,17 @@ open class AlwaysQuorum: CallQuorum { return resolved } - override fun record(response: ByteArray, upstream: Upstream<*>): Boolean { + override fun isFailed(): Boolean { + return false + } + + override fun record(response: ByteArray, upstream: Upstream): Boolean { result = response resolved = true return true } - override fun record(error: RpcException, upstream: Upstream<*>) { + override fun record(error: RpcException, upstream: Upstream) { } override fun getResult(): ByteArray? { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/BroadcastQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/BroadcastQuorum.kt index a3c39aa3..f87a0da7 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/BroadcastQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/BroadcastQuorum.kt @@ -16,14 +16,15 @@ */ package io.emeraldpay.dshackle.quorum +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.infinitape.etherjar.rpc.JacksonRpcConverter open class BroadcastQuorum( - jacksonRpcConverter: JacksonRpcConverter, + objectMapper: ObjectMapper, val quorum: Int = 3 -): CallQuorum, ValueAwareQuorum(jacksonRpcConverter, String::class.java) { +) : CallQuorum, ValueAwareQuorum(objectMapper, String::class.java) { private var result: ByteArray? = null private var txid: String? = null @@ -36,11 +37,15 @@ open class BroadcastQuorum( return calls >= quorum } + override fun isFailed(): Boolean { + return false + } + override fun getResult(): ByteArray? { return result } - override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) { + override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) { calls++ if (txid == null && responseValue != null) { txid = responseValue @@ -48,7 +53,7 @@ open class BroadcastQuorum( } } - override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) { + override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) { // can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low" calls++ if (result == null) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt index 99c1ac32..e6d3ae72 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt @@ -31,8 +31,10 @@ interface CallQuorum { fun init(head: Head) fun isResolved(): Boolean - fun record(response: ByteArray, upstream: Upstream<*>): Boolean - fun record(error: RpcException, upstream: Upstream<*>) + fun isFailed(): Boolean + + fun record(response: ByteArray, upstream: Upstream): Boolean + fun record(error: RpcException, upstream: Upstream) fun getResult(): ByteArray? companion object { @@ -42,8 +44,8 @@ interface CallQuorum { } } - fun asReducer(): BiFunction>, CallQuorum> { - return BiFunction>, CallQuorum> { a, b -> + fun asReducer(): BiFunction, CallQuorum> { + return BiFunction, CallQuorum> { a, b -> a.record(b.t1, b.t2) return@BiFunction a } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt index 094c646d..e2804696 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt @@ -16,15 +16,16 @@ */ package io.emeraldpay.dshackle.quorum +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.RpcException open class NonEmptyQuorum( - jacksonRpcConverter: JacksonRpcConverter, + objectMapper: ObjectMapper, val maxTries: Int = 3 -): CallQuorum, ValueAwareQuorum(jacksonRpcConverter, Any::class.java) { +) : CallQuorum, ValueAwareQuorum(objectMapper, Any::class.java) { private var result: ByteArray? = null private var tries: Int = 0 @@ -33,10 +34,14 @@ open class NonEmptyQuorum( } override fun isResolved(): Boolean { - return result != null || tries >= maxTries + return result != null } - override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*>) { + override fun isFailed(): Boolean { + return tries >= maxTries + } + + override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) { tries++ if (responseValue != null) { result = response @@ -47,10 +52,12 @@ open class NonEmptyQuorum( return result } - override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) { + override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) { + tries++ } - override fun record(error: RpcException, upstream: Upstream<*>) { + override fun record(error: RpcException, upstream: Upstream) { + tries++ } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonceQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonceQuorum.kt index a79840b1..d40f4beb 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonceQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonceQuorum.kt @@ -16,6 +16,7 @@ */ package io.emeraldpay.dshackle.quorum +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.infinitape.etherjar.hex.HexQuantity @@ -25,9 +26,9 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock open class NonceQuorum( - jacksonRpcConverter: JacksonRpcConverter, + objectMapper: ObjectMapper, val tries: Int = 3 -): CallQuorum, ValueAwareQuorum(jacksonRpcConverter, String::class.java) { +) : CallQuorum, ValueAwareQuorum(objectMapper, String::class.java) { private val lock = ReentrantLock() private var resultValue = 0L @@ -40,11 +41,15 @@ open class NonceQuorum( override fun isResolved(): Boolean { lock.withLock { - return receivedTimes >= tries || errors >= tries + return receivedTimes >= tries && !isFailed() } } - override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) { + override fun isFailed(): Boolean { + return errors >= tries + } + + override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) { val value = responseValue?.let { str -> HexQuantity.from(str).value.toLong() } @@ -63,11 +68,7 @@ open class NonceQuorum( return result } - override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) { - errors++ - } - - override fun record(error: RpcException, upstream: Upstream<*>) { + override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) { errors++ } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt index 91a149fd..bab164b5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt @@ -32,7 +32,11 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum { return result.get() != null } - override fun record(response: ByteArray, upstream: Upstream<*>): Boolean { + override fun isFailed(): Boolean { + return false + } + + override fun record(response: ByteArray, upstream: Upstream): Boolean { val lagging = upstream.getLag() > maxLag if (!lagging) { result.set(response) @@ -41,10 +45,9 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum { return false } - override fun record(error: RpcException, upstream: Upstream<*>) { + override fun record(error: RpcException, upstream: Upstream) { } - override fun getResult(): ByteArray { return result.get() } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumReaderFactory.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumReaderFactory.kt new file mode 100644 index 00000000..8e0727bb --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumReaderFactory.kt @@ -0,0 +1,38 @@ +/** + * 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.quorum + +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.ApiSource +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest + +// creates instance of a Quorum based reader +interface QuorumReaderFactory { + + companion object { + fun default(): QuorumReaderFactory { + return Default() + } + } + + fun create(apis: ApiSource, quorum: CallQuorum): Reader + + class Default : QuorumReaderFactory { + override fun create(apis: ApiSource, quorum: CallQuorum): Reader { + return QuorumRpcReader(apis, quorum) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt new file mode 100644 index 00000000..449a763b --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt @@ -0,0 +1,105 @@ +/** + * 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.quorum + +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.ApiSource +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.infinitape.etherjar.rpc.RpcException +import org.slf4j.LoggerFactory +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.util.function.Tuples + +/** + * Makes request with applying Quorum + */ +class QuorumRpcReader( + private val apis: ApiSource, + private val quorum: CallQuorum +) : Reader { + + companion object { + private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java) + } + + override fun read(key: JsonRpcRequest): Mono { + apis.request(1) + + // uses a mix of retry strategy and managed Publisher for calls. + // retry is used when an error happened + // but if no error received, we check quorum and if not enough data received we request more + // eventually source of upstreams is Completed (or something Errored) and if finalizes the result + + val retrySpec = reactor.util.retry.Retry.from { signal -> + signal.takeUntil { + it.totalRetries() >= 3 || quorum.isResolved() || quorum.isFailed() + }.doOnNext { + // need one more API source if retried + apis.request(1) + } + } + + return Flux.from(apis) + .flatMap { api -> + api.getApi().read(key) + .flatMap(JsonRpcResponse::requireResult) + // on error notify quorum, it may use error message or other details + .doOnError { err -> + if (err is RpcException) { + quorum.record(err, api) + } + } + .map { Tuples.of(it, api) } + } + .retryWhen(retrySpec) + // record all correct responses until quorum reached + .reduce(quorum, { res, a -> + if (res.record(a.t1, a.t2)) { + apis.resolve() + } else { + apis.request(1) + } + res + }) + // if last call resulted in error it's still possible that request was resolved correctly. i.e. for BroadcastQuorum + .onErrorResume { err -> + if (quorum.isResolved()) { + Mono.just(quorum) + } else { + Mono.error(err) + } + } + .doOnNext { + if (!it.isResolved()) { + log.debug("No quorum for ${key.method} as ${quorum}") + } + } + // return nothing if not resolved + .filter { it.isResolved() } + .map { + // TODO find actual quorum number + QuorumRpcReader.Result(it.getResult()!!, 1) + } + } + + + class Result( + val value: ByteArray, + val quorum: Int + ) +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt index 1148013f..3bed9837 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt @@ -16,23 +16,24 @@ */ package io.emeraldpay.dshackle.quorum +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.Upstream import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.RpcException import org.slf4j.LoggerFactory abstract class ValueAwareQuorum( - val jacksonRpcConverter: JacksonRpcConverter, + val objectMapper: ObjectMapper, val clazz: Class ): CallQuorum { private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java) fun extractValue(response: ByteArray, clazz: Class): T? { - return jacksonRpcConverter.fromJson(response.inputStream(), clazz) + return objectMapper.readValue(response.inputStream(), clazz) } - override fun record(response: ByteArray, upstream: Upstream<*>): Boolean { + override fun record(response: ByteArray, upstream: Upstream): Boolean { try { val value = extractValue(response, clazz) recordValue(response, value, upstream) @@ -44,12 +45,12 @@ abstract class ValueAwareQuorum( return isResolved(); } - override fun record(error: RpcException, upstream: Upstream<*>) { + override fun record(error: RpcException, upstream: Upstream) { recordError(null, error.rpcMessage, upstream) } - abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream<*>) + abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream) - abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) + abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/reader/Reader.kt b/src/main/kotlin/io/emeraldpay/dshackle/reader/Reader.kt index f5fcc7a8..5bcd2cd1 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/reader/Reader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/reader/Reader.kt @@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.reader import reactor.core.publisher.Mono -interface Reader { +interface Reader { fun read(key: K): Mono diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt index 82ebbcb0..45291ccf 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt @@ -29,15 +29,15 @@ import reactor.core.publisher.Mono @Service class Describe( - @Autowired private val upstreams: Upstreams, + @Autowired private val multistreamHolder: MultistreamHolder, @Autowired private val subscribeStatus: SubscribeStatus ) { fun describe(requestMono: Mono): Mono { return requestMono.map { _ -> val resp = BlockchainOuterClass.DescribeResponse.newBuilder() - upstreams.getAvailable().forEach { chain -> - upstreams.getUpstream(chain)?.let { chainUpstreams -> + multistreamHolder.getAvailable().forEach { chain -> + multistreamHolder.getUpstream(chain)?.let { chainUpstreams -> val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll()) val targets = chainUpstreams.getMethods().getSupportedMethods() val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt index 2c95ce94..078f8a66 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt @@ -24,8 +24,14 @@ import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.CallQuorum +import io.emeraldpay.dshackle.quorum.QuorumReaderFactory +import io.emeraldpay.dshackle.quorum.QuorumRpcReader +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.RpcResponseError import org.apache.commons.lang3.StringUtils import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -36,21 +42,23 @@ import java.lang.Exception @Service open class NativeCall( - @Autowired private val upstreams: Upstreams, + @Autowired private val multistreamHolder: MultistreamHolder, @Autowired private val objectMapper: ObjectMapper ) { private val log = LoggerFactory.getLogger(NativeCall::class.java) + var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default() + open fun nativeCall(requestMono: Mono): Flux { return requestMono.flatMapMany(this::prepareCall) .map(this::setupCallParams) .parallel() - .flatMap(this::fetch) - .sequential() - .map(this::buildResponse) - .doOnError { e -> log.warn("Error during native call: ${e.message}") } - .onErrorResume(this::processException) + .flatMap(this::fetch) + .sequential() + .map(this::buildResponse) + .doOnError { e -> log.warn("Error during native call: ${e.message}") } + .onErrorResume(this::processException) } fun setupCallParams(it: CallContext): CallContext { @@ -87,17 +95,17 @@ open class NativeCall( return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number))) } - if (!upstreams.isAvailable(chain)) { + if (!multistreamHolder.isAvailable(chain)) { return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number))) } - val upstream = upstreams.getUpstream(chain) + val upstream = multistreamHolder.getUpstream(chain) ?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain))) return prepareCall(request, upstream) } - fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<*>): Flux> { + fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: Multistream): Flux> { return request.itemsList.toFlux().map { val method = it.method val params = it.payload.toStringUtf8() @@ -115,75 +123,29 @@ open class NativeCall( } fun fetch(ctx: CallContext): Mono> { - return fetchFromCache(ctx) - .onErrorResume { t -> - log.warn("Failed to read from cache", t); - Mono.empty() - } - .switchIfEmpty( + return ctx.upstream.getRoutedApi(ctx.matcher) + .flatMap { api -> + api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params)) + .flatMap(JsonRpcResponse::requireResult) + .map { + ctx.withPayload(it) + } + }.switchIfEmpty( Mono.just(ctx).flatMap(this::executeOnRemote) ) - } - - fun fetchFromCache(ctx: CallContext): Mono> { - val cachingApi = ctx.upstream.cache - return cachingApi.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { ctx.withPayload(it) } + .onErrorMap { + CallFailure(ctx.id, it) + } } fun executeOnRemote(ctx: CallContext): Mono> { - val apis = ctx.getApis() - apis.request(1) - var failures = 0 - return Flux.from(apis) - .flatMap { api -> - val upstream = ctx.upstream - api.execute(ctx.id, ctx.payload.method, ctx.payload.params) - // on error notify quorum, it may use error message or other details - .doOnError { err -> - if (err is RpcException) { - ctx.callQuorum.record(err, upstream) - } - } - .map { Tuples.of(it, upstream) } - } - .retry { - failures++ - if (ctx.callQuorum.isResolved()) { - false - } else if (failures < 3) { - apis.request(1) - true - } else { - false - } - } - // record all correct responses until quorum reached - .reduce(ctx.callQuorum, {res, a -> - if (res.record(a.t1, a.t2)) { - apis.resolve() - } else { - apis.request(1) - } - res - }) - // if last call resulted in error it's still possible that request was resolved correctly. i.e. for BroadcastQuorum - .onErrorResume { err -> - if (ctx.callQuorum.isResolved()) { - Mono.just(ctx.callQuorum) - } else { - Mono.error(err) - } - } - .doOnNext { - if (!it.isResolved()) { - log.debug("No quorum for ${ctx.payload.method} as ${ctx.callQuorum}") - } - } - .filter { it.isResolved() } + if (!ctx.upstream.getMethods().isAllowed(ctx.payload.method)) { + return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method")) + } + val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum) + return reader.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params)) .map { - val result = it.getResult() - ?: throw CallFailure(ctx.id, Exception("No response from upstream for ${ctx.payload.method}")) - ctx.withPayload(result) + ctx.withPayload(it.value) } .onErrorMap { log.error("Failed to make a call", it) @@ -204,7 +166,7 @@ open class NativeCall( } open class CallContext(val id: Int, - val upstream: AggregatedUpstream<*>, + val upstream: Multistream, val matcher: Selector.Matcher, val callQuorum: CallQuorum, val payload: T) { @@ -212,8 +174,8 @@ open class NativeCall( return CallContext(id, upstream, matcher, callQuorum, payload) } - fun getApis(): ApiSource<*> { - return upstream.getApis(matcher) + fun getApis(): ApiSource { + return upstream.getApiSource(matcher) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt index d85d1eb6..620efcb2 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt @@ -21,10 +21,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.data.BlockContainer -import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.dshackle.upstream.MultistreamHolder import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -33,7 +31,7 @@ import reactor.core.publisher.Mono @Service class StreamHead( - @Autowired private val upstreams: Upstreams + @Autowired private val multistreamHolder: MultistreamHolder ) { private val log = LoggerFactory.getLogger(StreamHead::class.java) @@ -42,7 +40,7 @@ class StreamHead( return requestMono.map { request -> Chain.byId(request.type.number) }.flatMapMany { chain -> - val up = upstreams.getUpstream(chain) + val up = multistreamHolder.getUpstream(chain) ?: return@flatMapMany Flux.error(Exception("Unavailable chain: $chain")) up.getHead() .getFlux() @@ -64,7 +62,7 @@ class StreamHead( return BlockchainOuterClass.ChainHead.newBuilder() .setChainValue(chain.id) .setHeight(block.height) - .setTimestamp(block.timestamp!!.toEpochMilli()) + .setTimestamp(block.timestamp.toEpochMilli()) .setWeight(ByteString.copyFrom(block.difficulty.toByteArray())) .setBlockId(block.hash.toHex()) .build() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt index a2ad34d7..f22b2b17 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/SubscribeStatus.kt @@ -27,13 +27,13 @@ import reactor.core.publisher.Mono @Service class SubscribeStatus( - @Autowired private val upstreams: Upstreams + @Autowired private val multistreamHolder: MultistreamHolder ) { fun subscribeStatus(requestMono: Mono): Flux { return requestMono.flatMapMany { - val ups = upstreams.getAvailable().mapNotNull { chain -> - val chainUpstream = upstreams.getUpstream(chain) + val ups = multistreamHolder.getAvailable().mapNotNull { chain -> + val chainUpstream = multistreamHolder.getUpstream(chain) chainUpstream?.observeStatus()?.map { avail -> ChainSubscription(chain, chainUpstream, avail) } @@ -46,7 +46,7 @@ class SubscribeStatus( } } - fun chainStatus(chain: Chain, ups: List>): BlockchainOuterClass.ChainStatus { + fun chainStatus(chain: Chain, ups: List): BlockchainOuterClass.ChainStatus { val available = ups.map { u -> u.getStatus() }.min() ?: UpstreamAvailability.UNAVAILABLE @@ -60,6 +60,6 @@ class SubscribeStatus( .build() } - class ChainSubscription(val chain: Chain, val up: AggregatedUpstream<*>, val avail: UpstreamAvailability) + class ChainSubscription(val chain: Chain, val up: Multistream, val avail: UpstreamAvailability) } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt index f403f384..58896632 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt @@ -19,9 +19,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.SilentException -import io.emeraldpay.dshackle.upstream.Selector -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -35,7 +34,7 @@ import kotlin.collections.HashMap @Service class TrackBitcoinAddress( - @Autowired private val upstreams: Upstreams + @Autowired private val multistreamHolder: MultistreamHolder ) : TrackAddress { companion object { @@ -43,7 +42,7 @@ class TrackBitcoinAddress( } override fun isSupported(chain: Chain): Boolean { - return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain) + return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain) } fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List? { @@ -63,8 +62,8 @@ class TrackBitcoinAddress( } } - fun requestBalances(chain: Chain, api: DirectBitcoinApi, addresses: List): Flux { - return api.executeAndResult(0, "listunspent", emptyList(), List::class.java) + fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: List): Flux { + return api.getReader().listUnspent() .flatMapMany { unspents -> val result = getTotal(chain, addresses, unspents) Flux.fromIterable(result) @@ -73,17 +72,14 @@ class TrackBitcoinAddress( override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) - val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java) + val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) if (addresses.isEmpty()) { return Flux.empty() } - val result = upstream.getApi(Selector.empty).flatMapMany { api -> - requestBalances(chain, api, addresses) - .map(this@TrackBitcoinAddress::buildResponse) - } - return result + return requestBalances(chain, upstream, addresses) + .map(this@TrackBitcoinAddress::buildResponse) } fun getTotal(chain: Chain, addresses: List, unspents: List<*>): List { @@ -122,20 +118,16 @@ class TrackBitcoinAddress( override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) - val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java) + val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) if (addresses.isEmpty()) { return Flux.empty() } - val initial = upstream.getApi(Selector.empty).flatMapMany { api -> - requestBalances(chain, api, addresses) - } + val initial = requestBalances(chain, upstream, addresses) val following = upstream.getHead().getFlux() .flatMap { block -> - upstream.getApi(Selector.empty).flatMapMany { api -> - requestBalances(chain, api, addresses) - } + requestBalances(chain, upstream, addresses) } val last = HashMap() val result = Flux.merge(initial, following) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt index c2e8e40f..8199ff41 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt @@ -20,10 +20,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.SilentException -import io.emeraldpay.dshackle.upstream.Selector -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory @@ -39,7 +37,7 @@ import kotlin.math.min @Service class TrackBitcoinTx( - @Autowired private val upstreams: Upstreams + @Autowired private val multistreamHolder: MultistreamHolder ) : TrackTx { companion object { @@ -47,36 +45,35 @@ class TrackBitcoinTx( } override fun isSupported(chain: Chain): Boolean { - return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain) + return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain) } override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux { val chain = Chain.byId(request.chainValue) - val upstream = upstreams.getUpstream(chain)?.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java) + val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java) ?: return Flux.error(SilentException.UnsupportedBlockchain(chain)) val txid = request.txId val confirmations = max(min(1, request.confirmationLimit), 12) - return upstream.getApi(Selector.empty).flatMapMany { api -> - subscribe(chain, api, upstream, txid) - }.takeUntil { tx -> - tx.confirmations >= confirmations - }.map(this::asProto) + return subscribe(chain, upstream, txid) + .takeUntil { tx -> + tx.confirmations >= confirmations + }.map(this::asProto) } - fun subscribe(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux { - return loadExisting(api, txid) + fun subscribe(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux { + return loadExisting(upstream, txid) .flatMapMany { status -> if (status.mined) { //Head almost always knows the current height, so it can continue with calculating confirmations //without publishing an empty TxStatus first - continueWithMined(api, upstream, status) + continueWithMined(upstream, status) } else { loadMempool(upstream, txid) .flatMapMany { tx -> val next = if (tx.found) { untilMined(upstream, tx) } else { - untilFound(chain, api, upstream, txid) + untilFound(chain, upstream, txid) } //fist provide the current status, then updates Flux.concat(Mono.just(tx), next) @@ -85,8 +82,8 @@ class TrackBitcoinTx( } } - fun continueWithMined(api: DirectBitcoinApi, upstream: BitcoinUpstream, status: TxStatus): Flux { - return api.getBlock(status.blockHash!!) + fun continueWithMined(upstream: BitcoinMultistream, status: TxStatus): Flux { + return upstream.getReader().getBlock(status.blockHash!!) .map { block -> TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block)) }.flatMapMany { tx -> @@ -94,41 +91,40 @@ class TrackBitcoinTx( } } - fun untilFound(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux { + fun untilFound(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux { return Flux.interval(Duration.ofSeconds(1)) .take(Duration.ofMinutes(10)) .flatMap { loadMempool(upstream, txid) } .skipUntil { it.found } - .flatMap { subscribe(chain, api, upstream, txid) } + .flatMap { subscribe(chain, upstream, txid) } .doOnError { t -> log.error("Failed to wait until found", t) } } - fun untilMined(upstream: BitcoinUpstream, tx: TxStatus): Mono { + fun untilMined(upstream: BitcoinMultistream, tx: TxStatus): Mono { return upstream.getHead().getFlux().flatMap { - upstream.getApi(Selector.empty).flatMap { api -> - loadExisting(api, tx.txid) - }.filter { it.mined } + loadExisting(upstream, tx.txid) + .filter { it.mined } }.single() } - fun withConfirmations(upstream: BitcoinUpstream, tx: TxStatus): Flux { + fun withConfirmations(upstream: BitcoinMultistream, tx: TxStatus): Flux { return upstream.getHead().getFlux().map { tx.withHead(it.height) } } - fun loadExisting(api: DirectBitcoinApi, txid: String): Mono { - val mined = api.getTx(txid) + fun loadExisting(api: BitcoinMultistream, txid: String): Mono { + val mined = api.getReader().getTx(txid) return mined.map { val block = it["blockhash"] as String? TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it)) } } - fun loadMempool(upstream: BitcoinUpstream, txid: String): Mono { - val mempool = upstream.getData().getMempool().get() + fun loadMempool(upstream: BitcoinMultistream, txid: String): Mono { + val mempool = upstream.getReader().getMempool().get() return mempool.map { if (it.contains(txid)) { TxStatus(txid, found = true, mined = false) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt index ccc87424..b953d448 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt @@ -21,9 +21,8 @@ import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.SilentException -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.Wei @@ -35,13 +34,13 @@ import reactor.core.publisher.Mono @Service class TrackEthereumAddress( - @Autowired private val upstreams: Upstreams + @Autowired private val multistreamHolder: MultistreamHolder ) : TrackAddress { private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java) override fun isSupported(chain: Chain): Boolean { - return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain) + return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain) } override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux { @@ -52,7 +51,7 @@ class TrackEthereumAddress( override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) - val head = upstreams.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty() + val head = multistreamHolder.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty() val balances = initAddress(request) .flatMap { tracked -> val current = getBalance(tracked) @@ -87,14 +86,14 @@ class TrackEthereumAddress( } } - fun getUpstream(chain: Chain): AggregatedEthereumUpstreams { - return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java) + fun getUpstream(chain: Chain): EthereumMultistream { + return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java) ?: throw SilentException.UnsupportedBlockchain(chain) } private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) - if (!upstreams.isAvailable(chain)) { + if (!multistreamHolder.isAvailable(chain)) { return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) } if (request.asset.code?.toLowerCase() != "ether") { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt index f4df3d70..9ef50393 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt @@ -23,10 +23,8 @@ import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.TxId -import io.emeraldpay.dshackle.upstream.Upstream -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.TransactionId @@ -50,7 +48,7 @@ import kotlin.math.min @Service class TrackEthereumTx( - @Autowired private val upstreams: Upstreams + @Autowired private val multistreamHolder: MultistreamHolder ) : TrackTx { companion object { @@ -65,7 +63,7 @@ class TrackEthereumTx( private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java) override fun isSupported(chain: Chain): Boolean { - return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain) + return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain) } override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux { @@ -85,12 +83,12 @@ class TrackEthereumTx( } - fun getUpstream(chain: Chain): AggregatedEthereumUpstreams { - return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java) + fun getUpstream(chain: Chain): EthereumMultistream { + return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java) ?: throw SilentException.UnsupportedBlockchain(chain) } - fun subscribe(base: TxDetails, up: AggregatedEthereumUpstreams): Flux { + fun subscribe(base: TxDetails, up: EthereumMultistream): Flux { var latestTx = base val untilFound = Mono.just(latestTx) @@ -215,7 +213,7 @@ class TrackEthereumTx( } } - fun updateFromBlock(upstream: AggregatedEthereumUpstreams, tx: TxDetails, blockTx: TransactionJson): Mono { + fun updateFromBlock(upstream: EthereumMultistream, tx: TxDetails, blockTx: TransactionJson): Mono { return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) { val updated = tx.withStatus( blockHash = blockTx.blockHash, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index 8bb5f3db..8a62f67b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -19,19 +19,20 @@ package io.emeraldpay.dshackle.startup import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.FileResolver +import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.config.UpstreamsConfig -import io.emeraldpay.dshackle.upstream.CurrentUpstreams -import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream -import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs +import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcHttpClient +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository @@ -44,9 +45,10 @@ import kotlin.collections.HashMap @Repository open class ConfiguredUpstreams( @Autowired private val objectMapper: ObjectMapper, - @Autowired private val currentUpstreams: CurrentUpstreams, + @Autowired private val currentUpstreams: CurrentMultistreamHolder, @Autowired private val fileResolver: FileResolver, - @Autowired private val config: UpstreamsConfig + @Autowired private val config: UpstreamsConfig, + @Autowired private val cachesFactory: CachesFactory ) { private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java) @@ -134,80 +136,61 @@ open class ConfiguredUpstreams( options: UpstreamsConfig.Options) { val conn = config.connection!! - var rpcApi: DirectBitcoinApi? = null + val directApi: Reader? = buildHttpClient(config) + if (directApi == null) { + log.warn("Upstream doesn't have API configuration") + return + } + val methods = buildMethods(config, chain) - conn.rpc?.let { endpoint -> - val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) - rpcApi = DirectBitcoinApi(rpcClient, objectMapper, methods) - } - rpcApi?.let { api -> - val upstream = BitcoinUpstream(config.id - ?: "bitcoin-${seq.getAndIncrement()}", chain, api, - options, QuorumForLabels.QuorumItem(1, config.labels), - objectMapper, methods) - - upstream.start() - currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED)) - } + val upstream = BitcoinUpstream(config.id + ?: "bitcoin-${seq.getAndIncrement()}", chain, directApi, + options, QuorumForLabels.QuorumItem(1, config.labels), + objectMapper, methods) + upstream.start() + currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED)) } private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream, chain: Chain, options: UpstreamsConfig.Options) { val conn = config.connection!! - var rpcApi: DirectEthereumApi? = null + val directApi: Reader? = buildHttpClient(config) + if (directApi == null) { + log.warn("Upstream doesn't have API configuration") + return + } + val urls = ArrayList() val methods = buildMethods(config, chain) conn.rpc?.let { endpoint -> - val rpcClient = ReactorHttpRpcClient.newBuilder() - .connectTo(endpoint.url) - .alwaysSeparate() - conn.rpc?.basicAuth?.let { auth -> - rpcClient.basicAuth(auth.username, auth.password) - } - conn.rpc?.tls?.let { tls -> - tls.ca?.let { ca -> - fileResolver.resolve(ca).inputStream().use { cert -> rpcClient.trustedCertificate(cert) } - } - } - rpcApi = DirectEthereumApi( - rpcClient.build(), - null, - objectMapper, - methods - ).apply { - timeout = options.timeout - } - urls.add(endpoint.url) } - if (rpcApi != null) { - val wsApi: EthereumWs? = conn.ws?.let { endpoint -> - val wsApi = EthereumWs( - endpoint.url, - endpoint.origin ?: URI("http://localhost"), - rpcApi!!, - objectMapper - ) - endpoint.basicAuth?.let { auth -> - wsApi.basicAuth = auth - } - wsApi.connect() - urls.add(endpoint.url) - wsApi - } - log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}") - val ethereumUpstream = EthereumUpstream( - config.id!!, - chain, rpcApi!!, wsApi, options, - QuorumForLabels.QuorumItem(1, config.labels), - methods, - objectMapper) - ethereumUpstream.start() - currentUpstreams.update(UpstreamChange(chain, ethereumUpstream, UpstreamChange.ChangeType.ADDED)) + val wsFactoryApi: EthereumWsFactory? = conn.ws?.let { endpoint -> + val wsApi = EthereumWsFactory( + endpoint.url, + endpoint.origin ?: URI("http://localhost"), + objectMapper + ) + endpoint.basicAuth?.let { auth -> + wsApi.basicAuth = auth + } + urls.add(endpoint.url) + wsApi } + + log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}") + val ethereumUpstream = EthereumUpstream( + config.id!!, + chain, directApi, wsFactoryApi, options, + QuorumForLabels.QuorumItem(1, config.labels), + methods, + objectMapper + ) + ethereumUpstream.start() + currentUpstreams.update(UpstreamChange(chain, ethereumUpstream, UpstreamChange.ChangeType.ADDED)) } private fun buildGrpcUpstream(config: UpstreamsConfig.Upstream, options: UpstreamsConfig.Options) { @@ -230,5 +213,22 @@ open class ConfiguredUpstreams( .subscribe(currentUpstreams::update) } - + private fun buildHttpClient(config: UpstreamsConfig.Upstream): JsonRpcHttpClient? { + val conn = config.connection!! + val urls = ArrayList() + return conn.rpc?.let { endpoint -> + val tls = conn.rpc?.tls?.let { tls -> + tls.ca?.let { ca -> + fileResolver.resolve(ca).readBytes() + } + } + urls.add(endpoint.url) + JsonRpcHttpClient( + endpoint.url.toString(), + objectMapper, + conn.rpc?.basicAuth, + tls + ) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/UpstreamChange.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/UpstreamChange.kt index 4d637ee6..b1855430 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/UpstreamChange.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/UpstreamChange.kt @@ -32,7 +32,7 @@ class UpstreamChange( /** * Corresponding upstream */ - val upstream: Upstream<*>, + val upstream: Upstream, /** * Type of the change */ diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt index 4f7d77fe..1aceaf83 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt @@ -31,15 +31,24 @@ abstract class AbstractHead : Head { private val head = AtomicReference(null) private val stream: TopicProcessor = TopicProcessor.create() + private val beforeBlockHandlers = ArrayList() fun follow(source: Flux): Disposable { - return source.distinctUntilChanged { - it.hash - }.filter { block -> - val curr = head.get() - curr == null || curr.difficulty < block.difficulty - } + return source + .distinctUntilChanged { + it.hash + }.filter { block -> + val curr = head.get() + curr == null || curr.difficulty < block.difficulty + } + .doFinally { + // close internal stream if upstream is finished, otherwise it gets stuck + // but technically is should never happen during normal work, only when the Head + // is stopping + stream.onComplete() + } .subscribe { block -> + notifyBeforeBlock() val prev = head.getAndUpdate { curr -> if (curr == null || curr.difficulty < block.difficulty) { block @@ -54,6 +63,20 @@ abstract class AbstractHead : Head { } } + fun notifyBeforeBlock() { + beforeBlockHandlers.forEach { handler -> + try { + handler.run() + } catch (t: Throwable) { + log.warn("Before Block handler error", t) + } + } + } + + override fun onBeforeBlock(handler: Runnable) { + beforeBlockHandlers.add(handler) + } + override fun getFlux(): Flux { return Flux.merge( Mono.justOrEmpty(head.get()), diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt deleted file mode 100644 index 911bae9d..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2020 EmeraldPay, Inc - * Copyright (c) 2019 ETCDEV GmbH - * - * 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 - -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.cache.* -import io.emeraldpay.dshackle.config.UpstreamsConfig -import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods -import io.emeraldpay.dshackle.upstream.calls.CallMethods -import org.springframework.context.Lifecycle -import reactor.core.Disposable -import reactor.core.publisher.Flux -import java.time.Duration -import java.time.Instant -import java.util.concurrent.atomic.AtomicReference -import java.util.concurrent.locks.ReentrantLock -import java.util.function.Predicate -import kotlin.concurrent.withLock - -/** - * Aggregation of multiple upstreams responding to a single blockchain - */ -abstract class AggregatedUpstream( - private val objectMapper: ObjectMapper, - val caches: Caches -) : Upstream, Lifecycle { - - private var cacheSubscription: Disposable? = null - var cache: CachingEthereumApi = CachingEthereumApi.empty(objectMapper) - private val reconfigLock = ReentrantLock() - private var callMethods: CallMethods? = null - - abstract fun getAll(): List> - abstract fun addUpstream(upstream: Upstream) - abstract fun getApis(matcher: Selector.Matcher): ApiSource - - fun onUpstreamsUpdated() { - reconfigLock.withLock { - getAll().map { it.getMethods() }.let { - callMethods = AggregatedCallMethods(it) - } - } - } - - override fun observeStatus(): Flux { - val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } } - return Flux.merge(upstreamsFluxes) - .filter(FilterBestAvailability()) - .map { it.status } - } - - override fun isAvailable(): Boolean { - return getAll().any { it.isAvailable() } - } - - override fun getStatus(): UpstreamAvailability { - val upstreams = getAll() - return if (upstreams.isEmpty()) UpstreamAvailability.UNAVAILABLE - else upstreams.map { it.getStatus() }.min()!! - } - - override fun getOptions(): UpstreamsConfig.Options { - return UpstreamsConfig.Options() - } - - override fun getMethods(): CallMethods { - return callMethods ?: throw IllegalStateException("Methods are not initialized yet") - } - - override fun start() { - } - - override fun stop() { - cacheSubscription?.dispose() - cacheSubscription = null - } - - fun onHeadUpdated(head: Head) { - reconfigLock.withLock { - cacheSubscription?.dispose() - cacheSubscription = head.getFlux().subscribe { - caches.cache(Caches.Tag.LATEST, it) - } - cache = CachingEthereumApi(objectMapper, caches, head) - } - } - - // -------------------------------------------------------------------------------------------------------- - - class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now()) - - class FilterBestAvailability() : Predicate { - private val lastRef = AtomicReference() - - override fun test(t: UpstreamStatus): Boolean { - val last = lastRef.get() - val changed = last == null - || t.status > last.status - || (last.upstream == t.upstream && t.status != last.status) - || last.ts.isBefore(Instant.now() - Duration.ofSeconds(60)) - if (changed) { - lastRef.set(t) - } - return changed - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ApiSource.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ApiSource.kt index 7f37673f..2e62fec1 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ApiSource.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ApiSource.kt @@ -16,10 +16,12 @@ */ package io.emeraldpay.dshackle.upstream -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.reactivestreams.Publisher -interface ApiSource : Publisher { +interface ApiSource : Publisher { fun resolve() fun request(tries: Int) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt deleted file mode 100644 index 638a09a4..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright (c) 2020 EmeraldPay, Inc - * Copyright (c) 2019 ETCDEV GmbH - * - * 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 - -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.data.* -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.infinitape.etherjar.hex.HexQuantity -import org.slf4j.LoggerFactory -import reactor.core.publisher.Mono -import java.math.BigInteger -import java.util.function.Function - -open class CachingEthereumApi( - private val objectMapper: ObjectMapper, - private val caches: Caches, - private val head: Head -): EthereumApi(objectMapper) { - - companion object { - private val log = LoggerFactory.getLogger(CachingEthereumApi::class.java) - - /** - * Create caching API with empty memory-only cache - */ - @JvmStatic - fun empty(objectMapper: ObjectMapper): CachingEthereumApi { - return CachingEthereumApi(objectMapper, Caches.default(objectMapper), EmptyHead()) - } - } - - private val rawJsonBuilder = RawJsonBuilder() - - private val cacheBlocks = caches.getBlocksByHash() - private val cacheBlocksByHeight = caches.getBlocksByHeight() - private val cacheTx = caches.getTxByHash() - private val cacheFullBlocks = caches.getFullBlocks() - private val cacheFullBlocksByHeight = caches.getFullBlocksByHeight() - - fun readBlockByHash(id: Int, method: String, params: List): Mono { - return if (params.size == 2) { - val includeTransactions = params[1].toString().toBoolean() - val cache = if (includeTransactions) { - cacheFullBlocks - } else { - cacheBlocks - } - Mono.just(params[0]) - .map { BlockId.from(it as String) } - .flatMap(cache::read) - .transform(converter(id)) - .transform(finalizer()) - } - else Mono.empty() - } - - fun readBlockByNumber(id: Int, method: String, params: List): Mono { - return if (params.size == 2) { - val includeTransactions = params[1].toString().toBoolean() - val cache = if (includeTransactions) { - cacheFullBlocksByHeight - } else { - cacheBlocksByHeight - } - Mono.just(params[0]) - .map { HexQuantity.from(it as String) } - .filter { it.value < BigInteger.valueOf(Long.MAX_VALUE) } - .map { it.value.toLong() } - .flatMap(cache::read) - .transform(converter(id)) - .transform(finalizer()) - } - else Mono.empty() - } - - override fun execute(id: Int, method: String, params: List): Mono { - return when (method) { - "eth_blockNumber" -> - head.getFlux().next() - .map { HexQuantity.from(it.height).toHex() } - .map { objectMapper.writeValueAsBytes(it) } - .map(bytesToJson(id)) - "eth_getBlockByHash" -> readBlockByHash(id, method, params) - "eth_getBlockByNumber" -> readBlockByNumber(id, method, params) - "eth_getTransactionByHash" -> - if (params.size == 1) - Mono.just(params[0]) - .map { TxId.from(it as String) } - .flatMap(cacheTx::read) - .transform(converter(id)) - .transform(finalizer()) - else Mono.empty() - else -> - Mono.empty() - } - } - - /** - * Convert to JSON RPC response - */ - fun converter(id: Int): Function, out Mono> { - return Function { mono -> - mono.map(containerToJson(id)) - } - } - - /** - * Handle errors and other stuff - */ - fun finalizer(): Function, Mono> { - return Function { mono -> - mono.onErrorResume { t -> - log.warn("Error during read from cache", t) - Mono.empty() - } - } - } - - fun bytesToJson(id: Int): Function { - return Function { data -> - rawJsonBuilder.write(id, data) - } - } - - fun containerToJson(id: Int): Function { - return Function { data -> - rawJsonBuilder.write(id, data.json!!) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt deleted file mode 100644 index 7b5b33c0..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2020 EmeraldPay, Inc - * Copyright (c) 2019 ETCDEV GmbH - * - * 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 - -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.grpc.Chain -import org.slf4j.LoggerFactory -import org.springframework.context.Lifecycle -import reactor.core.Disposable -import reactor.core.publisher.Mono -import java.lang.IllegalStateException -import java.time.Duration - -/** - * General interface to upstream(s) to a single chain - */ -abstract class ChainUpstreams( - val chain: Chain, - private val upstreams: MutableList>, - caches: Caches, - objectMapper: ObjectMapper -) : AggregatedUpstream(objectMapper, caches), Lifecycle { - - private val log = LoggerFactory.getLogger(ChainUpstreams::class.java) - private var seq = 0 - protected var lagObserver: HeadLagObserver? = null - private var subscription: Disposable? = null - - open fun init() { - onUpstreamsUpdated() - } - - abstract fun updateHead(): Head - abstract fun setHead(head: Head) - - override fun getId(): String { - return "!all:${chain.chainCode}" - } - - override fun isRunning(): Boolean { - return subscription != null - } - - override fun start() { - super.start() - subscription = observeStatus() - .distinctUntilChanged() - .subscribe { printStatus() } - } - - override fun stop() { - super.stop() - subscription?.dispose() - subscription = null - getHead().let { - if (it is Lifecycle) { - it.stop() - } - } - lagObserver?.stop() - } - - override fun getAll(): List> { - return upstreams - } - - override fun addUpstream(upstream: Upstream) { - upstreams.add(upstream) - setHead(updateHead()) - onUpstreamsUpdated() - } - - fun removeUpstream(id: String) { - if (upstreams.removeIf { it.getId() == id }) { - setHead(updateHead()) - onUpstreamsUpdated() - } - } - - override fun getApis(matcher: Selector.Matcher): ApiSource { - val i = seq++ - if (seq >= Int.MAX_VALUE / 2) { - seq = 0 - } - return FilteredApis(upstreams, matcher, i) - } - - override fun getApi(matcher: Selector.Matcher): Mono { - val apis = getApis(matcher) - apis.request(1) - return Mono.from(apis) - .switchIfEmpty(Mono.error(Exception("No API available"))) - } - - override fun setLag(lag: Long) { - } - - override fun getLag(): Long { - return 0 - } - - fun printStatus() { - var height: Long? = null - try { - height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height - } catch (e: IllegalStateException) { - //timout - } catch (e: Exception) { - log.warn("Head processing error: ${e.javaClass} ${e.message}") - } - val statuses = upstreams.map { it.getStatus() } - .groupBy { it } - .map { "${it.key.name}/${it.value.size}" } - .joinToString(",") - val lag = upstreams.map { it.getLag() } - .joinToString(", ") - - log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]") - } -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentMultistreamHolder.kt similarity index 76% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentMultistreamHolder.kt index 84f733df..efe076e6 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentMultistreamHolder.kt @@ -21,14 +21,12 @@ import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.startup.UpstreamChange -import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream -import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods +import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory @@ -44,14 +42,14 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @Repository -class CurrentUpstreams( +class CurrentMultistreamHolder( @Autowired private val objectMapper: ObjectMapper, @Autowired private val cachesFactory: CachesFactory -): Upstreams { +) : MultistreamHolder { - private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java) + private val log = LoggerFactory.getLogger(CurrentMultistreamHolder::class.java) - private val chainMapping = ConcurrentHashMap>() + private val chainMapping = ConcurrentHashMap() private val chainsBus = TopicProcessor.create() private val callTargets = HashMap() private val updateLock = ReentrantLock() @@ -61,20 +59,18 @@ class CurrentUpstreams( val chain = change.chain when (BlockchainType.fromBlockchain(chain)) { BlockchainType.ETHEREUM -> { - val up = change.upstream - .cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream - val current = chainMapping[chain] as ChainUpstreams? + val up = change.upstream.cast(EthereumUpstream::class.java) + val current = chainMapping[chain] as Multistream? val factory = Callable { - AggregatedEthereumUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams + EthereumMultistream(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as Multistream } processUpdate(change, up, current, factory) } BlockchainType.BITCOIN -> { - val up = change.upstream - .cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java) - val current = chainMapping[chain] as ChainUpstreams? + val up = change.upstream.cast(BitcoinUpstream::class.java) + val current = chainMapping[chain] as Multistream? val factory = Callable { - BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams + BitcoinMultistream(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as Multistream } processUpdate(change, up, current, factory) } @@ -85,7 +81,7 @@ class CurrentUpstreams( } } - fun processUpdate(change: UpstreamChange, up: Upstream, current: ChainUpstreams?, factory: Callable>) { + fun processUpdate(change: UpstreamChange, up: Upstream, current: Multistream?, factory: Callable) { val chain = change.chain if (change.type == UpstreamChange.ChangeType.REMOVED) { current?.removeUpstream(up.getId()) @@ -113,7 +109,7 @@ class CurrentUpstreams( } } - override fun getUpstream(chain: Chain): AggregatedUpstream<*>? { + override fun getUpstream(chain: Chain): Multistream? { return chainMapping[chain] } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt index 7503f40e..7a223b45 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt @@ -22,13 +22,13 @@ import reactor.core.publisher.Flux import reactor.core.publisher.TopicProcessor import java.util.concurrent.atomic.AtomicReference -abstract class DefaultUpstream( +abstract class DefaultUpstream( private val id: String, defaultLag: Long, defaultAvail: UpstreamAvailability, private val options: UpstreamsConfig.Options, private val targets: CallMethods? -) : Upstream { +) : Upstream { constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt index 9957d0c7..db0f97ea 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt @@ -25,4 +25,7 @@ class EmptyHead : Head { override fun getFlux(): Flux { return Flux.empty() } + + override fun onBeforeBlock(handler: Runnable) { + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteredApis.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteredApis.kt index f38ff9eb..93845663 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteredApis.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/FilteredApis.kt @@ -16,6 +16,9 @@ */ package io.emeraldpay.dshackle.upstream +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.reactivestreams.Subscriber import reactor.core.publisher.EmitterProcessor import reactor.core.publisher.Flux @@ -26,28 +29,28 @@ import kotlin.math.pow import kotlin.math.roundToLong import kotlin.random.Random -class FilteredApis( - allUpstreams: List>, +class FilteredApis( + allUpstreams: List, private val matcher: Selector.Matcher, pos: Int, private val repeatLimit: Long, jitter: Int -) : ApiSource { +) : ApiSource { companion object { private const val DEFAULT_DELAY_STEP = 100 private const val MAX_WAIT_MILLIS = 5000L } - constructor(allUpstreams: List>, + constructor(allUpstreams: List, matcher: Selector.Matcher, pos: Int) : this(allUpstreams, matcher, pos, 10, 7) - constructor(allUpstreams: List>, + constructor(allUpstreams: List, matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10) private val delay: Int - private val upstreams: List> + private val upstreams: List private val control = EmitterProcessor.create(32, false) @@ -75,18 +78,18 @@ class FilteredApis( return Duration.ofMillis(time) } - override fun subscribe(subscriber: Subscriber) { + override fun subscribe(subscriber: Subscriber) { val first = Flux.fromIterable(upstreams) val retries = (1 until repeatLimit).map { r -> Flux.fromIterable(upstreams).delaySubscription(waitDuration(r)) }.let { Flux.concat(it) } Flux.concat(first, retries) - .filter(Upstream::isAvailable) + .filter(Upstream::isAvailable) .filter(matcher::matches) - .flatMap { it.getApi(matcher) } - .zipWith(control).map { it.t1 } - .subscribe(subscriber as Subscriber) + .zipWith(control) + .map { it.t1 } + .subscribe(subscriber) } override fun resolve() { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt index 092cc80b..ffac632f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt @@ -20,6 +20,20 @@ import io.emeraldpay.dshackle.data.BlockContainer import reactor.core.publisher.Flux import reactor.core.publisher.Mono +/** + * Subscription to listen to updates to the head of a blockchain. + */ interface Head { + + /** + * @return stream of all new blocks, starts from the current block (i.e., first item should be available immediately). + */ fun getFlux(): Flux + + /** + * Add handler that is going to be called each time _before_ a new block is submitted to stream of new blocks. + * Supposed to be used for cleanup/preparation before actual block data will come, to avoid race condition. + * @see getFlux + */ + fun onBeforeBlock(handler: Runnable) } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/HeadLagObserver.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/HeadLagObserver.kt index 74e09b7a..9f8a1911 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/HeadLagObserver.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/HeadLagObserver.kt @@ -28,9 +28,9 @@ import reactor.util.function.Tuples * Observer group of upstreams and defined a distance in blocks (lag) between a leader (best height/difficulty) and * other upstreams. */ -abstract class HeadLagObserver( +abstract class HeadLagObserver( private val master: Head, - private val followers: Collection> + private val followers: Collection ) : Lifecycle { private val log = LoggerFactory.getLogger(HeadLagObserver::class.java) @@ -58,7 +58,7 @@ abstract class HeadLagObserver( } } - fun probeFollowers(top: BlockContainer): Flux>> { + fun probeFollowers(top: BlockContainer): Flux> { return Flux.fromIterable(followers) .parallel(followers.size) .flatMap { mapLagging(top, it, getCurrentBlocks(it)) } @@ -66,9 +66,9 @@ abstract class HeadLagObserver( .onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) } } - abstract fun getCurrentBlocks(up: Upstream): Flux + abstract fun getCurrentBlocks(up: Upstream): Flux - fun mapLagging(top: BlockContainer, up: Upstream, blocks: Flux): Flux>> { + fun mapLagging(top: BlockContainer, up: Upstream, blocks: Flux): Flux> { return blocks .map { extractDistance(top, it) } .takeUntil { lag -> lag <= 0L } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt new file mode 100644 index 00000000..62c34e44 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2020 EmeraldPay, Inc + * Copyright (c) 2019 ETCDEV GmbH + * + * 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 + +import io.emeraldpay.dshackle.cache.* +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.emeraldpay.grpc.Chain +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle +import reactor.core.Disposable +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration +import java.time.Instant +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock +import java.util.function.Predicate +import kotlin.concurrent.withLock + +/** + * Aggregation of multiple upstreams responding to a single blockchain + */ +abstract class Multistream( + val chain: Chain, + private val upstreams: MutableList, + val caches: Caches +) : Upstream, Lifecycle { + + companion object { + private val log = LoggerFactory.getLogger(Multistream::class.java) + } + + private var cacheSubscription: Disposable? = null + private val reconfigLock = ReentrantLock() + private var callMethods: CallMethods? = null + private var seq = 0 + protected var lagObserver: HeadLagObserver? = null + private var subscription: Disposable? = null + + open fun init() { + onUpstreamsUpdated() + } + + /** + * Get list of all underlying upstreams + */ + fun getAll(): List { + return upstreams + } + + /** + * Add an upstream + */ + fun addUpstream(upstream: Upstream) { + upstreams.add(upstream) + setHead(updateHead()) + onUpstreamsUpdated() + } + + fun removeUpstream(id: String) { + if (upstreams.removeIf { it.getId() == id }) { + setHead(updateHead()) + onUpstreamsUpdated() + } + } + + /** + * Get a source for direct APIs + */ + fun getApiSource(matcher: Selector.Matcher): ApiSource { + val i = seq++ + if (seq >= Int.MAX_VALUE / 2) { + seq = 0 + } + return FilteredApis(upstreams, matcher, i) + } + + /** + * Finds an API that executed directly on a remote. + */ + fun getDirectApi(matcher: Selector.Matcher): Mono> { + val apis = getApiSource(matcher) + apis.request(1) + return Mono.from(apis) + .map(Upstream::getApi) + .switchIfEmpty(Mono.error(Exception("No API available for $chain"))) + } + + /** + * Finds an API that leverages caches and other optimizations/transformations of the request. + */ + abstract fun getRoutedApi(matcher: Selector.Matcher): Mono> + + override fun getApi(): Reader { + throw NotImplementedError("Immediate direct API is not implemented for Aggregated Upstream") + } + + fun onUpstreamsUpdated() { + reconfigLock.withLock { + getAll().map { it.getMethods() }.let { + callMethods = AggregatedCallMethods(it) + } + } + } + + override fun observeStatus(): Flux { + val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } } + return Flux.merge(upstreamsFluxes) + .filter(FilterBestAvailability()) + .map { it.status } + } + + override fun isAvailable(): Boolean { + return getAll().any { it.isAvailable() } + } + + override fun getStatus(): UpstreamAvailability { + val upstreams = getAll() + return if (upstreams.isEmpty()) UpstreamAvailability.UNAVAILABLE + else upstreams.map { it.getStatus() }.min()!! + } + + override fun getOptions(): UpstreamsConfig.Options { + return UpstreamsConfig.Options() + } + + override fun getMethods(): CallMethods { + return callMethods ?: throw IllegalStateException("Methods are not initialized yet") + } + + override fun start() { + subscription = observeStatus() + .distinctUntilChanged() + .subscribe { printStatus() } + } + + override fun stop() { + cacheSubscription?.dispose() + cacheSubscription = null + subscription?.dispose() + subscription = null + getHead().let { + if (it is Lifecycle) { + it.stop() + } + } + lagObserver?.stop() + } + + fun onHeadUpdated(head: Head) { + reconfigLock.withLock { + cacheSubscription?.dispose() + cacheSubscription = head.getFlux().subscribe { + caches.cache(Caches.Tag.LATEST, it) + } + } + } + + abstract fun updateHead(): Head + abstract fun setHead(head: Head) + + override fun getId(): String { + return "!all:${chain.chainCode}" + } + + override fun isRunning(): Boolean { + return subscription != null + } + + override fun setLag(lag: Long) { + } + + override fun getLag(): Long { + return 0 + } + + fun printStatus() { + var height: Long? = null + try { + height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height + } catch (e: java.lang.IllegalStateException) { + //timout + } catch (e: Exception) { + log.warn("Head processing error: ${e.javaClass} ${e.message}") + } + val statuses = upstreams.map { it.getStatus() } + .groupBy { it } + .map { "${it.key.name}/${it.value.size}" } + .joinToString(",") + val lag = upstreams.map { it.getLag() } + .joinToString(", ") + + log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]") + } + + // -------------------------------------------------------------------------------------------------------- + + class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now()) + + class FilterBestAvailability() : Predicate { + private val lastRef = AtomicReference() + + override fun test(t: UpstreamStatus): Boolean { + val last = lastRef.get() + val changed = last == null + || t.status > last.status + || (last.upstream == t.upstream && t.status != last.status) + || last.ts.isBefore(Instant.now() - Duration.ofSeconds(60)) + if (changed) { + lastRef.set(t) + } + return changed + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MultistreamHolder.kt similarity index 88% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/MultistreamHolder.kt index 57a2f3ab..e9f85f68 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MultistreamHolder.kt @@ -20,8 +20,11 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.grpc.Chain import reactor.core.publisher.Flux -interface Upstreams { - fun getUpstream(chain: Chain): AggregatedUpstream<*>? +/** + * Holds Multistreams configured for a chain. + */ +interface MultistreamHolder { + fun getUpstream(chain: Chain): Multistream? fun getAvailable(): List fun observeChains(): Flux fun getDefaultMethods(chain: Chain): CallMethods diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt index 82630e1c..1e309031 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt @@ -96,13 +96,13 @@ class Selector { } interface Matcher { - fun matches(up: Upstream): Boolean + fun matches(up: Upstream): Boolean } class MultiMatcher( private val matchers: Collection ): Matcher { - override fun matches(up: Upstream): Boolean { + override fun matches(up: Upstream): Boolean { return matchers.all { it.matches(up) } } @@ -114,13 +114,13 @@ class Selector { class MethodMatcher( val method: String ): Matcher { - override fun matches(up: Upstream): Boolean { + override fun matches(up: Upstream): Boolean { return up.getMethods().isAllowed(method) } } abstract class LabelSelectorMatcher: Matcher { - override fun matches(up: Upstream): Boolean { + override fun matches(up: Upstream): Boolean { return up.getLabels().any(this::matches) } @@ -129,7 +129,7 @@ class Selector { } class EmptyMatcher: Matcher { - override fun matches(up: Upstream): Boolean { + override fun matches(up: Upstream): Boolean { return true } } @@ -144,7 +144,7 @@ class Selector { return null } - override fun matches(up: Upstream): Boolean { + override fun matches(up: Upstream): Boolean { return true } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt index 9a3005ce..3af96e79 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt @@ -17,16 +17,19 @@ package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import reactor.core.publisher.Flux import reactor.core.publisher.Mono -interface Upstream { +interface Upstream { fun isAvailable(): Boolean fun getStatus(): UpstreamAvailability fun observeStatus(): Flux fun getHead(): Head - fun getApi(matcher: Selector.Matcher): Mono + fun getApi(): Reader fun getOptions(): UpstreamsConfig.Options fun setLag(lag: Long) fun getLag(): Long @@ -34,6 +37,5 @@ interface Upstream { fun getMethods(): CallMethods fun getId(): String - fun castApi(apiType: Class): Upstream - fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T + fun cast(selfType: Class): T } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinMultistream.kt similarity index 66% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinMultistream.kt index eefb7ed9..6a75e7d9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinMultistream.kt @@ -18,24 +18,29 @@ package io.emeraldpay.dshackle.upstream.bitcoin import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.reader.EmptyReader +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.* -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle +import reactor.core.publisher.Mono -class BitcoinChainUpstreams( +open class BitcoinMultistream( chain: Chain, val upstreams: MutableList, caches: Caches, - objectMapper: ObjectMapper -) : ChainUpstreams(chain, upstreams as MutableList>, caches, objectMapper) { + private val objectMapper: ObjectMapper +) : Multistream(chain, upstreams as MutableList, caches), Lifecycle { companion object { - private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java) + private val log = LoggerFactory.getLogger(BitcoinMultistream::class.java) } private var head: Head? = null + private var reader = BitcoinReader(this, EmptyHead(), objectMapper) override fun init() { if (upstreams.size > 0) { @@ -68,8 +73,18 @@ class BitcoinChainUpstreams( return head } + override fun getRoutedApi(matcher: Selector.Matcher): Mono> { + //TODO + return Mono.just(EmptyReader()) + } + + open fun getReader(): BitcoinReader { + return reader + } + override fun setHead(head: Head) { this.head = head + reader = BitcoinReader(this, head, objectMapper) } override fun getHead(): Head { @@ -80,18 +95,24 @@ class BitcoinChainUpstreams( return upstreams.flatMap { it.getLabels() } } - override fun castApi(apiType: Class): Upstream { - if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") - } - return this as Upstream - } - - override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { + override fun cast(selfType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - return castApi(apiType) as T + return this as T } + override fun isRunning(): Boolean { + return super.isRunning() || reader.isRunning + } + + override fun start() { + super.start() + reader.start() + } + + override fun stop() { + super.stop() + reader.stop() + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinReader.kt index d736a41a..86f5ad3d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinReader.kt @@ -15,25 +15,44 @@ */ package io.emeraldpay.dshackle.upstream.bitcoin +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle +import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.cast open class BitcoinReader( - api: DirectBitcoinApi, - head: Head + private val upstreams: BitcoinMultistream, + head: Head, + private val objectMapper: ObjectMapper ) : Lifecycle { companion object { private val log = LoggerFactory.getLogger(BitcoinReader::class.java) } - private val mempool = CachingMempoolData(api, head) + private val mempool = CachingMempoolData(upstreams, head, objectMapper) open fun getMempool(): CachingMempoolData { return mempool } + open fun getBlock(hash: String): Mono> { + return castedRead(JsonRpcRequest("getblock", listOf(hash)), Map::class.java).cast() + } + + open fun getTx(txid: String): Mono> { + return castedRead(JsonRpcRequest("getrawtransaction", listOf(txid, true)), Map::class.java).cast() + } + + open fun listUnspent(): Mono> { + return castedRead(JsonRpcRequest("listunspent", emptyList()), List::class.java).cast() + } + override fun isRunning(): Boolean { return mempool.isRunning } @@ -45,4 +64,14 @@ open class BitcoinReader( override fun stop() { mempool.stop() } + + fun castedRead(req: JsonRpcRequest, clazz: Class): Mono { + return upstreams.getDirectApi(Selector.empty).flatMap { api -> + api.read(req) + .flatMap(JsonRpcResponse::requireResult) + .map { + objectMapper.readValue(it, clazz) as T + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt index 2a5aaca0..bf7830df 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt @@ -16,8 +16,11 @@ package io.emeraldpay.dshackle.upstream.bitcoin import io.emeraldpay.dshackle.Defaults +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.AbstractHead import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import org.springframework.scheduling.concurrent.CustomizableThreadFactory @@ -29,7 +32,7 @@ import java.time.Duration import java.util.concurrent.Executors class BitcoinRpcHead( - private val api: DirectBitcoinApi, + private val api: Reader, private val extractBlock: ExtractBlock, private val interval: Duration = Duration.ofSeconds(15) ) : Head, AbstractHead(), Lifecycle { @@ -53,12 +56,14 @@ class BitcoinRpcHead( val base = Flux.interval(interval) .publishOn(scheduler) .flatMap { - api.executeAndResult(0, "getbestblockhash", emptyList(), String::class.java) + api.read(JsonRpcRequest("getbestblockhash", emptyList())) + .flatMap(JsonRpcResponse::requireStringResult) .timeout(Defaults.timeout, Mono.error(Exception("Best block hash is not received"))) } .distinctUntilChanged() .flatMap { hash -> - api.execute(0, "getblock", listOf(hash)) + api.read(JsonRpcRequest("getblock", listOf(hash))) + .flatMap(JsonRpcResponse::requireResult) .map(extractBlock::extract) .timeout(Defaults.timeout, Mono.error(Exception("Block data is not received"))) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt index 14441e09..30a46bb3 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -17,9 +17,12 @@ package io.emeraldpay.dshackle.upstream.bitcoin import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle @@ -29,12 +32,12 @@ import reactor.core.publisher.Mono open class BitcoinUpstream( id: String, val chain: Chain, - private val api: DirectBitcoinApi, + private val directApi: Reader, options: UpstreamsConfig.Options, val node: QuorumForLabels.QuorumItem, private val objectMapper: ObjectMapper, callMethods: CallMethods -) : DefaultUpstream(id, options, callMethods), Lifecycle { +) : DefaultUpstream(id, options, callMethods), Lifecycle { companion object { private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) @@ -42,43 +45,31 @@ open class BitcoinUpstream( private val head: Head = createHead() private var validatorSubscription: Disposable? = null - private val data = BitcoinReader(api, head) private fun createHead(): Head { return BitcoinRpcHead( - api, + directApi, ExtractBlock(objectMapper) ) } - open fun getData(): BitcoinReader { - return data - } - override fun getHead(): Head { return head } - override fun getApi(matcher: Selector.Matcher): Mono { - return Mono.just(api) + override fun getApi(): Reader { + return directApi } override fun getLabels(): Collection { return listOf(UpstreamsConfig.Labels()) } - override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { + override fun cast(selfType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - return castApi(apiType) as T - } - - override fun castApi(apiType: Class): Upstream { - if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) { - throw ClassCastException("Cannot cast ${DirectBitcoinApi::class.java} to $apiType") - } - return this as Upstream + return this as T } override fun isRunning(): Boolean { @@ -86,7 +77,7 @@ open class BitcoinUpstream( if (head is Lifecycle) { runningAny = runningAny || head.isRunning } - runningAny = runningAny || data.isRunning + runningAny = runningAny return runningAny } @@ -97,7 +88,6 @@ open class BitcoinUpstream( head.start() } } - data.start() validatorSubscription?.dispose() @@ -105,7 +95,7 @@ open class BitcoinUpstream( this.setLag(0) this.setStatus(UpstreamAvailability.OK) } else { - val validator = BitcoinUpstreamValidator(api, getOptions()) + val validator = BitcoinUpstreamValidator(directApi, getOptions()) validatorSubscription = validator.start() .subscribe(this::setStatus) } @@ -115,9 +105,7 @@ open class BitcoinUpstream( if (head is Lifecycle) { head.stop() } - data.stop() validatorSubscription?.dispose() } - } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt index 255cf2bf..e800afe4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt @@ -16,7 +16,10 @@ package io.emeraldpay.dshackle.upstream.bitcoin import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.slf4j.LoggerFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory import reactor.core.publisher.Flux @@ -26,7 +29,7 @@ import java.time.Duration import java.util.concurrent.Executors class BitcoinUpstreamValidator( - private val api: DirectBitcoinApi, + private val api: Reader, private val options: UpstreamsConfig.Options ) { @@ -36,7 +39,9 @@ class BitcoinUpstreamValidator( } fun validate(): Mono { - return api.executeAndResult(0, "getconnectioncount", emptyList(), Int::class.java) + return api.read(JsonRpcRequest("getconnectioncount", emptyList())) + .flatMap(JsonRpcResponse::requireResult) + .map { Integer.parseInt(String(it)) } .map { count -> val minPeers = options.minPeers ?: 1 if (count < minPeers) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt index 66b4f8cc..d18356ba 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt @@ -15,7 +15,11 @@ */ package io.emeraldpay.dshackle.upstream.bitcoin +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable @@ -26,8 +30,9 @@ import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock open class CachingMempoolData( - private val api: DirectBitcoinApi, - private val head: Head + private val upstreams: BitcoinMultistream, + private val head: Head, + private val objectMapper: ObjectMapper ) : Lifecycle { companion object { @@ -56,7 +61,11 @@ open class CachingMempoolData( } fun fetchFromUpstream(): Mono> { - return api.executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono> + return upstreams.getDirectApi(Selector.empty).flatMap { api -> + api.read(JsonRpcRequest("getrawmempool", emptyList())) + .flatMap(JsonRpcResponse::requireResult) + .map { objectMapper.readValue(it, List::class.java) as List } + } } class Container(val since: Instant, val value: List) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApi.kt deleted file mode 100644 index d6d3406f..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApi.kt +++ /dev/null @@ -1,122 +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.bitcoin - -import com.fasterxml.jackson.databind.JavaType -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.upstream.UpstreamApi -import io.emeraldpay.dshackle.upstream.calls.CallMethods -import io.grpc.Status -import io.grpc.StatusRuntimeException -import io.infinitape.etherjar.rpc.RpcException -import io.infinitape.etherjar.rpc.RpcResponseError -import io.infinitape.etherjar.rpc.json.FullResponseJson -import io.infinitape.etherjar.rpc.json.RequestJson -import io.infinitape.etherjar.rpc.json.ResponseJson -import org.slf4j.LoggerFactory -import reactor.core.publisher.Mono - -open class DirectBitcoinApi( - val bitcoinRpcClient: BitcoinRpcClient, - val objectMapper: ObjectMapper, - val targets: CallMethods -) : UpstreamApi { - - companion object { - private val log = LoggerFactory.getLogger(DirectBitcoinApi::class.java) - } - - open override fun execute(id: Int, method: String, params: List): Mono { - //TODO it's almost the same code as for DirectEthereumApi; refactor - val result: Mono = when { - targets.isHardcoded(method) -> Mono.just(method).map { targets.executeHardcoded(it) } - targets.isAllowed(method) -> executeAndResult(id, method, params, Object::class.java) - else -> Mono.error(RpcException(-32601, "Method not allowed or not found")) - } - return processResult(id, method, result) - } - - public fun processResult(id: Int, method: String, result: Mono): Mono { - //TODO it's the same code as for DirectEthereumApi; refactor - return result - .doOnError { t -> - log.warn("Upstream error: [${t.message}] for $method") - } - .map { - val resp = ResponseJson() - resp.id = id - resp.result = it - resp - } - .switchIfEmpty( - Mono.fromCallable { - val resp = ResponseJson() - resp.id = id - resp.result = null - resp - } - ) - .map { - objectMapper.writer().writeValueAsBytes(it) - } - .onErrorResume(StatusRuntimeException::class.java) { t -> - if (t.status.code == Status.Code.CANCELLED) { - Mono.empty() - } else { - Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, "gRPC error ${t.status}")) - } - } - .onErrorMap { t -> - if (RpcException::class.java.isAssignableFrom(t.javaClass)) { - t - } else { - log.warn("Convert to RPC error. Exception ${t.javaClass}:${t.message}", t) - RpcException(-32020, "Error reading from upstream", null, t) - } - } - .onErrorResume(RpcException::class.java) { t -> - val resp = ResponseJson() - resp.id = id - resp.error = t.error - Mono.just(objectMapper.writer().writeValueAsBytes(resp)) - } - } - - open fun executeAndResult(id: Int, method: String, params: List, resultType: Class): Mono { - val rpc = RequestJson(method, params, id) - return Mono.just(rpc) - .map(objectMapper::writeValueAsBytes) - .flatMap(bitcoinRpcClient::execute) - .flatMap { json -> - val type: JavaType = objectMapper.typeFactory.constructParametricType(FullResponseJson::class.java, resultType, Int::class.java) - val resp = objectMapper.readerFor(type).readValue>(json) - if (resp.hasError()) { - Mono.error(resp.error.asException()) - } else { - Mono.just(resp.result) - } - } - } - - open fun getBlock(hash: String): Mono> { - return executeAndResult(0, "getblock", listOf(hash), Map::class.java) as Mono> - } - - open fun getTx(txid: String): Mono> { - return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono> - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt index 63f92fe7..daa3dab5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt @@ -63,6 +63,7 @@ class ExtractBlock( getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"), false, json, + data, transactions ) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethods.kt index c21c9063..3739bd1b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethods.kt @@ -40,7 +40,7 @@ class AggregatedCallMethods( */ override fun getQuorumFor(method: String): CallQuorum { return delegates.find { - it.isAllowed(method) + it.isAllowed(method) || it.isHardcoded(method) }?.getQuorumFor(method) ?: throw IllegalStateException("No quorum for $method") } @@ -62,15 +62,15 @@ class AggregatedCallMethods( * @return true if there is at least one delegate that allows the method and it's hardcoded on that delegate */ override fun isHardcoded(method: String): Boolean { - return delegates.any { it.isAllowed(method) && it.isHardcoded(method) } + return delegates.any { it.isHardcoded(method) } } /** * Executed the method on the first delegate that supports it as a hardcoded method */ - override fun executeHardcoded(method: String): Any { + override fun executeHardcoded(method: String): ByteArray { return delegates.find { - it.isAllowed(method) && it.isHardcoded(method) + it.isHardcoded(method) }?.executeHardcoded(method) ?: throw IllegalStateException("No hardcoded for $method") } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/CallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/CallMethods.kt index 99d63835..368979bb 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/CallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/CallMethods.kt @@ -46,5 +46,5 @@ interface CallMethods { /** * Read [supposed to be predefined] method from this config */ - fun executeHardcoded(method: String): Any + fun executeHardcoded(method: String): ByteArray } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultBitcoinMethods.kt similarity index 80% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultBitcoinMethods.kt index 9f1c0ded..4308041a 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultBitcoinMethods.kt @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream.bitcoin +package io.emeraldpay.dshackle.upstream.calls import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.quorum.* -import io.emeraldpay.dshackle.upstream.calls.CallMethods -import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.RpcException import java.util.* @@ -26,9 +24,6 @@ class DefaultBitcoinMethods( private val objectMapper: ObjectMapper ) : CallMethods { - //TODO maybe Ethereum RPC parser should not be really used for Bitcoin - private val jacksonRpcConverter = JacksonRpcConverter(objectMapper) - private val anyResponseMethods = listOf( "getblock", "gettransaction", "getrawtransaction", "gettxout", @@ -55,7 +50,7 @@ class DefaultBitcoinMethods( Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum() Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2) Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0) - Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(jacksonRpcConverter) + Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(objectMapper) else -> AlwaysQuorum() } } @@ -72,13 +67,10 @@ class DefaultBitcoinMethods( return Collections.binarySearch(hardcodedMethods, method) >= 0; } - override fun executeHardcoded(method: String): Any { + override fun executeHardcoded(method: String): ByteArray { return when (method) { - "getconnectioncount" -> 42 - "getnetworkinfo" -> mapOf( - "version" to 700000, - "subversion" to "/EmeraldDshackle:v0.7/" - ) + "getconnectioncount" -> "42".toByteArray() + "getnetworkinfo" -> "{\"version\": 700000, \"subversion\": \"/EmeraldDshackle:v0.7/\"}".toByteArray() else -> throw RpcException(-32601, "Method not found") } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt index ac91ca05..3eca4c3c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultEthereumMethods.kt @@ -32,8 +32,6 @@ class DefaultEthereumMethods( private val chain: Chain ) : CallMethods { - private val jacksonRpcConverter = JacksonRpcConverter(objectMapper) - private val anyResponseMethods = listOf( "eth_gasPrice", "eth_call", @@ -90,9 +88,9 @@ class DefaultEthereumMethods( headVerifiedMethods.contains(method) -> NotLaggingQuorum(1) specialMethods.contains(method) -> { when (method) { - "eth_getTransactionCount" -> NonceQuorum(jacksonRpcConverter) + "eth_getTransactionCount" -> NonceQuorum(objectMapper) "eth_getBalance" -> NotLaggingQuorum(1) - "eth_sendRawTransaction" -> BroadcastQuorum(jacksonRpcConverter) + "eth_sendRawTransaction" -> BroadcastQuorum(objectMapper) else -> AlwaysQuorum() } } @@ -108,50 +106,55 @@ class DefaultEthereumMethods( return hardcodedMethods.contains(method) } - override fun executeHardcoded(method: String): Any { - if ("net_version" == method) { - if (Chain.ETHEREUM == chain) { - return "1" + override fun executeHardcoded(method: String): ByteArray { + val json = when (method) { + "net_version" -> { + when { + Chain.ETHEREUM == chain -> { + "1" + } + Chain.ETHEREUM_CLASSIC == chain -> { + "1" + } + Chain.TESTNET_MORDEN == chain -> { + "2" + } + Chain.TESTNET_KOVAN == chain -> { + "42" + } + else -> throw RpcException(-32602, "Invalid chain") + } } - if (Chain.ETHEREUM_CLASSIC == chain) { - return "1" + "net_peerCount" -> { + "\"0x2a\"" } - if (Chain.TESTNET_MORDEN == chain) { - return "2" + "net_listening" -> { + "true" } - if (Chain.TESTNET_KOVAN == chain) { - return "42" + "web3_clientVersion" -> { + "\"EmeraldDshackle/v0.2\"" } - throw RpcException(-32602, "Invalid chain") + "eth_protocolVersion" -> { + "\"0x3f\"" + } + "eth_syncing" -> { + "false" + } + "eth_coinbase" -> { + "\"0x0000000000000000000000000000000000000000\"" + } + "eth_mining" -> { + "false" + } + "eth_hashrate" -> { + "\"0x0\"" + } + "eth_accounts" -> { + "[]" + } + else -> throw RpcException(-32601, "Method not found") } - if ("net_peerCount" == method) { - return "0x2a" - } - if ("net_listening" == method) { - return true - } - if ("web3_clientVersion" == method) { - return "EmeraldDshackle/v0.2" - } - if ("eth_protocolVersion" == method) { - return "0x3f" - } - if ("eth_syncing" == method) { - return false - } - if ("eth_coinbase" == method) { - return "0x0000000000000000000000000000000000000000" - } - if ("eth_mining" == method) { - return "false" - } - if ("eth_hashrate" == method) { - return "0x0" - } - if ("eth_accounts" == method) { - return Collections.emptyList() - } - throw RpcException(-32601, "Method not found") + return json.toByteArray() } override fun getSupportedMethods(): Set { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt index 65056683..7bc619a6 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt @@ -44,7 +44,7 @@ open class DirectCallMethods(private val methods: Set) : CallMethods { return false } - override fun executeHardcoded(method: String): Any { - return "unsupported" + override fun executeHardcoded(method: String): ByteArray { + return "unsupported".toByteArray() } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/ManagedCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/ManagedCallMethods.kt index 32e091ec..dfce3481 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/ManagedCallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/ManagedCallMethods.kt @@ -55,7 +55,7 @@ class ManagedCallMethods( return delegate.isHardcoded(method) } - override fun executeHardcoded(method: String): Any { + override fun executeHardcoded(method: String): ByteArray { return delegate.executeHardcoded(method) } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApi.kt deleted file mode 100644 index 3f10cf27..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApi.kt +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright (c) 2020 EmeraldPay, Inc - * Copyright (c) 2019 ETCDEV GmbH - * - * 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 com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.Defaults -import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.upstream.calls.CallMethods -import io.grpc.Status -import io.grpc.StatusRuntimeException -import io.infinitape.etherjar.domain.BlockHash -import io.infinitape.etherjar.domain.TransactionId -import io.infinitape.etherjar.hex.HexQuantity -import io.infinitape.etherjar.rpc.* -import io.infinitape.etherjar.rpc.json.ResponseJson -import org.slf4j.LoggerFactory -import reactor.core.publisher.Mono -import java.math.BigInteger - -open class DirectEthereumApi( - val rpcClient: ReactorRpcClient, - var caches: Caches?, - private val objectMapper: ObjectMapper, - val targets: CallMethods -): EthereumApi(objectMapper) { - - var timeout = Defaults.timeout - private val log = LoggerFactory.getLogger(EthereumApi::class.java) - - override fun execute(id: Int, method: String, params: List): Mono { - val result: Mono = when { - targets.isHardcoded(method) -> Mono.just(method).map { targets.executeHardcoded(it) } - targets.isAllowed(method) -> callUpstream(method, params) - else -> Mono.error(RpcException(-32601, "Method not allowed or not found")) - } - return processResult(id, method, result) - } - - public fun processResult(id: Int, method: String, result: Mono): Mono { - return result - .doOnError { t -> - log.warn("Upstream error: [${t.message}] for $method") - } - .map { - val resp = ResponseJson() - resp.id = id - resp.result = it - resp - } - .switchIfEmpty( - Mono.fromCallable { - val resp = ResponseJson() - resp.id = id - resp.result = null - resp - } - ) - .map { - objectMapper.writer().writeValueAsBytes(it) - } - .onErrorResume(StatusRuntimeException::class.java) { t -> - if (t.status.code == Status.Code.CANCELLED) { - Mono.empty() - } else { - Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, "gRPC error ${t.status}")) - } - } - .onErrorMap { t -> - if (RpcException::class.java.isAssignableFrom(t.javaClass)) { - t - } else { - log.warn("Convert to RPC error. Exception ${t.javaClass}:${t.message}", t) - RpcException(-32020, "Error reading from upstream", null, t) - } - } - .onErrorResume(RpcException::class.java) { t -> - val resp = ResponseJson() - resp.id = id - resp.error = t.error - Mono.just(objectMapper.writer().writeValueAsBytes(resp)) - } - } - - /** - * Actual request to the remote endpoint - */ - private fun callUpstream(method: String, params: List): Mono { - return rpcClient.execute(callMapping(method, params)) - .timeout(timeout, Mono.error(RpcException(-32603, "Upstream timeout"))) - .doOnNext { value -> - try { - caches?.cacheRequested(value) - } catch (e: Throwable) { - //ignore all caching errors, client shouldn't have problems because of them - log.warn("Uncaught caching exception", e) - } - } - } - - /** - * Prepare RpcCall with data types specific for that particular requests. In general it may return a call that just - * parses JSON into Map. But the purpose of further processing and caching for some of the requests we want - * to have actual data types. - */ - fun callMapping(method: String, params: List): RpcCall { - return when { - method == "eth_getTransactionByHash" -> { - if (params.size != 1) { - throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 1 parameter") - } - val hash: TransactionId - try { - hash = TransactionId.from(params[0].toString()) - } catch (e: IllegalArgumentException) { - throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be transaction id") - } - Commands.eth().getTransaction(hash) - } - method == "eth_getBlockByHash" -> { - if (params.size != 2) { - throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters") - } - val hash: BlockHash - try { - hash = BlockHash.from(params[0].toString()) - } catch (e: IllegalArgumentException) { - throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block hash") - } - val withTx = params[1].toString().toBoolean() - if (withTx) { - Commands.eth().getBlockWithTransactions(hash) - } else { - Commands.eth().getBlock(hash) - } - } - 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() - number = quantity.value.let { - if (it < BigInteger.valueOf(Long.MAX_VALUE) && it >= BigInteger.ZERO) { - it.toLong() - } else { - 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) { - Commands.eth().getBlockWithTransactions(number) - } else { - Commands.eth().getBlock(number) - } - } - else -> RpcCall.create(method, Any::class.java, params) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumApi.kt deleted file mode 100644 index b3e12e63..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumApi.kt +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2020 EmeraldPay, Inc - * Copyright (c) 2019 ETCDEV GmbH - * - * 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 com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.reader.Reader -import io.emeraldpay.dshackle.upstream.Upstream -import io.emeraldpay.dshackle.upstream.UpstreamApi -import io.infinitape.etherjar.rpc.* -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson -import org.slf4j.LoggerFactory -import reactor.core.publisher.Mono -import java.io.InputStream - -abstract class EthereumApi( - objectMapper: ObjectMapper -) : UpstreamApi { - - companion object { - private val log = LoggerFactory.getLogger(EthereumApi::class.java) - } - - private val jacksonRpcConverter = JacksonRpcConverter(objectMapper) - var upstream: Upstream? = null - - fun reader(): Reader, RS> { - return object : Reader, RS> { - override fun read(key: RpcCall): Mono { - return this@EthereumApi.executeAndConvert(key) - } - } - } - - fun execute(rpcCall: RpcCall): Mono { - return execute(0, rpcCall.method, rpcCall.params as List) - } - - fun executeAndConvert(rpcCall: RpcCall): Mono { - val convertToJS = java.util.function.Function> { resp -> - val inputStream: InputStream = resp.inputStream() - val jsonValue: JS? = jacksonRpcConverter.fromJson(inputStream, rpcCall.jsonType, Int::class.java) - if (jsonValue == null) Mono.empty() - else Mono.just(jsonValue) - } - return execute(rpcCall) - .flatMap(convertToJS) - .map(rpcCall.converter::apply) - .doOnError { err -> log.debug("Failed to read from upstream", err) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/cache/EthereumBlocksWithTxCache.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumFullBlocksReader.kt similarity index 93% rename from src/main/kotlin/io/emeraldpay/dshackle/cache/EthereumBlocksWithTxCache.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumFullBlocksReader.kt index ad55c073..1db6bec6 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/cache/EthereumBlocksWithTxCache.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumFullBlocksReader.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.cache +package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.data.BlockContainer @@ -30,20 +30,20 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono /** - * Reads blocks with full transactions details. Based on data contained in cashes for blocks - * and transactions, i.e. two separate caches that must be provided. + * Reads blocks with full transactions details. Based on data contained in readers for blocks + * and transactions, i.e. two separate readers that must be provided. * * If source block, with just transaction hashes is not available, it returns empty * If any of the expected block transactions is not available it returns empty */ -class EthereumBlocksWithTxCache( +class EthereumFullBlocksReader( private val objectMapper: ObjectMapper, private val blocks: Reader, private val txes: Reader ) : Reader { companion object { - private val log = LoggerFactory.getLogger(EthereumBlocksWithTxCache::class.java) + private val log = LoggerFactory.getLogger(EthereumFullBlocksReader::class.java) } override fun read(key: BlockId): Mono { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt index e7994969..1ae739d9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt @@ -26,14 +26,14 @@ import java.time.Duration class EthereumHeadLagObserver( master: Head, - followers: Collection> -) : HeadLagObserver(master, followers) { + followers: Collection +) : HeadLagObserver(master, followers) { companion object { private val log = LoggerFactory.getLogger(EthereumHeadLagObserver::class.java) } - override fun getCurrentBlocks(up: Upstream): Flux { + override fun getCurrentBlocks(up: Upstream): Flux { val head = up.getHead() return head.getFlux().take(Duration.ofSeconds(1)) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/AggregatedEthereumUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumMultistream.kt similarity index 79% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/AggregatedEthereumUpstreams.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumMultistream.kt index 72e9f8c1..c607b503 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/AggregatedEthereumUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumMultistream.kt @@ -19,20 +19,24 @@ package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.* +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle +import reactor.core.publisher.Mono -open class AggregatedEthereumUpstreams( +open class EthereumMultistream( chain: Chain, val upstreams: MutableList, caches: Caches, - objectMapper: ObjectMapper -) : ChainUpstreams(chain, upstreams as MutableList>, caches, objectMapper) { + private val objectMapper: ObjectMapper +) : Multistream(chain, upstreams as MutableList, caches) { companion object { - private val log = LoggerFactory.getLogger(AggregatedEthereumUpstreams::class.java) + private val log = LoggerFactory.getLogger(EthereumMultistream::class.java) } private var head: Head? = null @@ -92,7 +96,7 @@ open class AggregatedEthereumUpstreams( val newHead = MergedHead(upstreams.map { it.getHead() }).apply { this.start() } - val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection>).apply { + val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection).apply { this.start() } this.lagObserver = lagObserver @@ -107,18 +111,15 @@ open class AggregatedEthereumUpstreams( } @SuppressWarnings("unchecked") - override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { + override fun cast(selfType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - return castApi(apiType) as T + return this as T } - override fun castApi(apiType: Class): Upstream { - if (!apiType.isAssignableFrom(EthereumApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") - } - return this as Upstream + override fun getRoutedApi(matcher: Selector.Matcher): Mono> { + return Mono.just(NativeCallRouter(objectMapper, reader, getMethods())) } } \ 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 5a540bbb..8dadd1a9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumReader.kt @@ -18,24 +18,21 @@ package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CurrentBlockCache -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.data.* import io.emeraldpay.dshackle.reader.* -import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.Selector -import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.domain.Wei -import io.infinitape.etherjar.rpc.Commands -import io.infinitape.etherjar.rpc.RpcCall +import io.infinitape.etherjar.hex.HexQuantity +import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.BlockTag import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory @@ -48,7 +45,7 @@ import java.util.concurrent.TimeoutException import java.util.function.Function open class EthereumReader( - private val up: Upstream, + private val up: Multistream, private val caches: Caches, private val objectMapper: ObjectMapper ) : Lifecycle { @@ -57,51 +54,109 @@ open class EthereumReader( private val log = LoggerFactory.getLogger(EthereumReader::class.java) } - private var headListener: Disposable? = null private val balanceCache = CurrentBlockCache() - private val extractBlock = Function> { block -> - objectMapper - .readValue(block.json, BlockJson::class.java) - .withoutTransactionDetails() + val extractBlock = Function> { block -> + val existing = block.getParsed(BlockJson::class.java) + if (existing != null) { + existing.withoutTransactionDetails() + } else { + objectMapper + .readValue(block.json, BlockJson::class.java) + .withoutTransactionDetails() + } } - private val extractTx = Function { tx -> - objectMapper - .readValue(tx.json, TransactionJson::class.java) + val extractTx = Function { tx -> + tx.getParsed(TransactionJson::class.java) ?: objectMapper.readValue(tx.json, TransactionJson::class.java) } - private val blocksDirect: Reader> - private val txDirect: Reader + val asRaw = Function { tx -> + tx.json ?: ByteArray(0) + } + + val jsonToRaw = Function { json -> + objectMapper.writeValueAsBytes(json) + } + + val blockAsContainer = Function, BlockContainer> { block -> + BlockContainer.from(block.withoutTransactionDetails(), objectMapper) + } + val txAsContainer = Function { tx -> + TxContainer.from(tx, objectMapper) + } + + private val blocksDirect: Reader + private val blocksByHeightDirect: Reader + private val txDirect: Reader private val balanceDirect: Reader private val idToBlockHash = Function { id -> BlockHash.from(id.value) } private val blockHashToId = Function { hash -> BlockId.from(hash) } private val txHashToId = Function { hash -> TxId.from(hash) } + private val idToTxHash = Function { id -> TransactionId.from(id.value) } + + private val directResponseBytes = Function { resp -> + if (resp.error != null) { + throw resp.error.asException() + } else { + resp.getResult() + } + } init { - blocksDirect = object : Reader> { - override fun read(key: BlockHash): Mono> { - return up.getApi(Selector.empty).flatMap { api -> - api.executeAndConvert(Commands.eth().getBlock(key)) + blocksDirect = object : Reader { + override fun read(key: BlockHash): Mono { + return up.getDirectApi(Selector.empty).flatMap { api -> + val request = JsonRpcRequest("eth_getBlockByHash", listOf(key.toHex(), false)) + api.read(request) .timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key"))) + .map(directResponseBytes) .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) + .map { blockbytes -> + val block = objectMapper.readValue(blockbytes, BlockJson::class.java) as BlockJson + BlockContainer.from(block, blockbytes) + } .doOnNext { block -> - caches.cache(Caches.Tag.REQUESTED, BlockContainer.from(block, objectMapper)) + caches.cache(Caches.Tag.REQUESTED, block) } } } } - txDirect = object : Reader { - override fun read(key: TransactionId): Mono { - return up.getApi(Selector.empty).flatMap { api -> - api.executeAndConvert(Commands.eth().getTransaction(key)) - .timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key"))) + blocksByHeightDirect = object : Reader { + override fun read(key: Long): Mono { + return up.getDirectApi(Selector.empty).flatMap { api -> + val request = JsonRpcRequest("eth_getBlockByNumber", listOf(HexQuantity.from(key).toHex(), false)) + api.read(request) + .timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key"))) + .map(directResponseBytes) .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) + .map { blockbytes -> + val block = objectMapper.readValue(blockbytes, BlockJson::class.java) as BlockJson + BlockContainer.from(block, blockbytes) + } + .doOnNext { block -> + caches.cache(Caches.Tag.REQUESTED, block) + } + } + } + } + txDirect = object : Reader { + override fun read(key: TransactionId): Mono { + return up.getDirectApi(Selector.empty).flatMap { api -> + val request = JsonRpcRequest("eth_getTransactionByHash", listOf(key.toHex())) + api.read(request) + .timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key"))) + .map(directResponseBytes) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) + .map { txbytes -> + val tx = objectMapper.readValue(txbytes, TransactionJson::class.java) + TxContainer.from(tx, txbytes) + } .doOnNext { tx -> - if (tx.blockNumber != null && tx.blockHash != null) { - caches.cache(Caches.Tag.REQUESTED, TxContainer.from(tx, objectMapper)) + if (tx.blockId != null) { + caches.cache(Caches.Tag.REQUESTED, tx) } } } @@ -109,9 +164,19 @@ open class EthereumReader( } balanceDirect = object : Reader { override fun read(key: Address): Mono { - return up.getApi(Selector.empty).flatMap { api -> - api.executeAndConvert(Commands.eth().getBalance(key, BlockTag.LATEST)) + return up.getDirectApi(Selector.empty).flatMap { api -> + val request = JsonRpcRequest("eth_getBalance", listOf(key.toHex(), "latest")) + api.read(request) .timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Balance not read $key"))) + .map(directResponseBytes) + .map { + val str = String(it) + if (str.startsWith("\"") && str.endsWith("\"")) { + Wei.from(str.substring(1, str.length - 1)) + } else { + throw RpcException(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Not Wei value") + } + } .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) .doOnNext { value -> balanceCache.put(key, value) @@ -121,30 +186,61 @@ open class EthereumReader( } } - fun blocksById(): Reader> { - return CompoundReader( - TransformingReader(caches.getBlocksByHash(), extractBlock), - RekeyingReader(idToBlockHash, blocksDirect) + fun blocksByHash(): Reader> { + return TransformingReader( + CompoundReader( + RekeyingReader(blockHashToId, caches.getBlocksByHash()), + blocksDirect + ), + extractBlock ) } - fun blocksByHash(): Reader> { - return CompoundReader( - RekeyingReader( - blockHashToId, - TransformingReader(caches.getBlocksByHash(), extractBlock) + fun blocksById(): Reader> { + return TransformingReader( + CompoundReader( + caches.getBlocksByHash(), + RekeyingReader(idToBlockHash, blocksDirect) ), - blocksDirect + extractBlock + ) + } + + fun blocksByHashAsCont(): Reader { + return TransformingReader( + blocksByHash(), + blockAsContainer + ) + } + + fun blocksByIdAsCont(): Reader { + return TransformingReader( + blocksById(), + blockAsContainer + ) + } + + fun blocksByHeightAsCont(): Reader { + return CompoundReader( + caches.getBlocksByHeight(), + blocksByHeightDirect ) } fun txByHash(): Reader { - return CompoundReader( - RekeyingReader( - txHashToId, - TransformingReader(caches.getTxByHash(), extractTx) + return TransformingReader( + CompoundReader( + RekeyingReader(txHashToId, caches.getTxByHash()), + txDirect ), - txDirect + extractTx + ) + } + + fun txByHashAsCont(): Reader { + return CompoundReader( + caches.getTxByHash(), + RekeyingReader(idToTxHash, txDirect) ) } @@ -155,18 +251,17 @@ open class EthereumReader( } override fun isRunning(): Boolean { - return this.headListener != null + //TODO should be always running? + return up.isRunning } override fun start() { - this.headListener = up.getHead().getFlux().subscribe { + val evictCaches: Runnable = Runnable { balanceCache.evict() } + up.getHead().onBeforeBlock(evictCaches) } override fun stop() { - val headListener = this.headListener - this.headListener = null - headListener?.dispose() } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt index c1c2db4d..334ddf66 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt @@ -19,6 +19,10 @@ package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.infinitape.etherjar.hex.HexQuantity import io.infinitape.etherjar.rpc.Commands import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle @@ -31,7 +35,7 @@ import java.time.Duration import java.util.concurrent.Executors class EthereumRpcHead( - private val api: DirectEthereumApi, + private val api: Reader, private val objectMapper: ObjectMapper, private val interval: Duration = Duration.ofSeconds(10) ): DefaultEthereumHead(), Lifecycle { @@ -48,21 +52,27 @@ class EthereumRpcHead( val base = Flux.interval(interval) .publishOn(scheduler) .flatMap { - api.rpcClient - .execute(Commands.eth().blockNumber) + api.read(JsonRpcRequest("eth_blockNumber", emptyList())) .subscribeOn(scheduler) .timeout(Defaults.timeout, Mono.error(Exception("Block number not received"))) + .flatMap { + if (it.error != null) { + Mono.error(it.error.asException()) + } else { + val value = it.getResultAsProcessedString() + Mono.just(HexQuantity.from(value)) + } + } } .flatMap { //fetching by Block Height here, critical to use same upstream, //different upstreams may have different blocks on the same height - api.rpcClient - .execute(Commands.eth().getBlock(it)) + api.read(JsonRpcRequest("eth_getBlockByNumber", listOf(it.toHex(), false))) .subscribeOn(scheduler) .timeout(Defaults.timeout, Mono.error(Exception("Block data not received"))) } .map { - BlockContainer.from(it, objectMapper) + BlockContainer.from(it.getResult(), objectMapper) } .onErrorContinue { err, _ -> log.debug("RPC error ${err.message}") diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt index cfdb162d..556757b2 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt @@ -20,10 +20,13 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle @@ -34,15 +37,15 @@ import java.time.Duration open class EthereumUpstream( id: String, val chain: Chain, - private val api: DirectEthereumApi, - private val ethereumWs: EthereumWs? = null, + private val directReader: Reader, + private val ethereumWsFactory: EthereumWsFactory? = null, options: UpstreamsConfig.Options, val node: QuorumForLabels.QuorumItem, targets: CallMethods, private val objectMapper: ObjectMapper -) : DefaultUpstream(id, options, targets), Upstream, CachesEnabled, Lifecycle { +) : DefaultUpstream(id, options, targets), Upstream, CachesEnabled, Lifecycle { - constructor(id: String, chain: Chain, api: DirectEthereumApi, objectMapper: ObjectMapper) : this(id, chain, api, null, + constructor(id: String, chain: Chain, api: Reader, objectMapper: ObjectMapper) : this(id, chain, api, null, UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()), DirectCallMethods(), objectMapper) @@ -52,12 +55,7 @@ open class EthereumUpstream( private val head: Head = this.createHead() private var validatorSubscription: Disposable? = null - init { - api.upstream = this - } - override fun setCaches(caches: Caches) { - api.caches = caches; if (head is CachesEnabled) { head.setCaches(caches) } @@ -70,7 +68,7 @@ open class EthereumUpstream( this.setLag(0) this.setStatus(UpstreamAvailability.OK) } else { - val validator = EthereumUpstreamValidator(this, getOptions()) + val validator = EthereumUpstreamValidator(this, getOptions(), objectMapper) validatorSubscription = validator.start() .subscribe(this::setStatus) } @@ -89,21 +87,24 @@ open class EthereumUpstream( } open fun createHead(): Head { - return if (ethereumWs != null) { - val ws = EthereumWsHead(ethereumWs).apply { - this.start() + return if (ethereumWsFactory != null) { + val ws = ethereumWsFactory.create(this).apply { + connect() } - // receive bew blocks through Websockets, but periodically verify with RPC - val rpc = EthereumRpcHead(api, objectMapper, Duration.ofSeconds(30)).apply { - this.start() + val wsHead = EthereumWsHead(ws).apply { + start() } - MergedHead(listOf(rpc, ws)).apply { - this.start() + // receive bew blocks through WebSockets, but also periodically verify with RPC in case if WS failed + val rpcHead = EthereumRpcHead(getApi(), objectMapper, Duration.ofSeconds(60)).apply { + start() + } + MergedHead(listOf(rpcHead, wsHead)).apply { + start() } } else { log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC") - EthereumRpcHead(api, objectMapper).apply { - this.start() + EthereumRpcHead(getApi(), objectMapper).apply { + start() } } } @@ -112,8 +113,8 @@ open class EthereumUpstream( return head } - override fun getApi(matcher: Selector.Matcher): Mono { - return Mono.just(api) + override fun getApi(): Reader { + return directReader } override fun getLabels(): Collection { @@ -121,18 +122,11 @@ open class EthereumUpstream( } @Suppress("unchecked") - override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { + override fun cast(selfType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - return castApi(apiType) as T - } - - override fun castApi(apiType: Class): Upstream { - if (!apiType.isAssignableFrom(EthereumApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") - } - return this as Upstream + return this as T } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt index abf982dd..488dccb1 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt @@ -16,11 +16,13 @@ */ package io.emeraldpay.dshackle.upstream.ethereum +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.config.UpstreamsConfig -import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.UpstreamAvailability -import io.infinitape.etherjar.rpc.* +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.infinitape.etherjar.rpc.json.SyncingJson import org.slf4j.LoggerFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory import reactor.core.publisher.Flux @@ -30,8 +32,9 @@ import java.time.Duration import java.util.concurrent.Executors class EthereumUpstreamValidator( - private val ethereumUpstream: EthereumUpstream, - private val options: UpstreamsConfig.Options + private val upstream: EthereumUpstream, + private val options: UpstreamsConfig.Options, + private val objectMapper: ObjectMapper ) { companion object { private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java) @@ -39,30 +42,32 @@ class EthereumUpstreamValidator( } fun validate(): Mono { - return ethereumUpstream - .getApi(Selector.empty) - .flatMapMany { api -> - api.rpcClient - .execute(Commands.eth().syncing()) - .timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing"))) - .flatMap { value -> - if (value.isSyncing) { - Mono.just(UpstreamAvailability.SYNCING) - } else { - api.rpcClient.execute(Commands.net().peerCount()) - .timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers"))) - .map { count -> - val minPeers = options.minPeers ?: 1 - if (count < minPeers) { - UpstreamAvailability.IMMATURE - } else { - UpstreamAvailability.OK - } - } + return upstream + .getApi() + .read(JsonRpcRequest("eth_syncing", listOf())) + .flatMap(JsonRpcResponse::requireResult) + .map { objectMapper.readValue(it, SyncingJson::class.java) } + .timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing"))) + .flatMap { value -> + if (value.isSyncing) { + Mono.just(UpstreamAvailability.SYNCING) + } else { + upstream + .getApi() + .read(JsonRpcRequest("net_peerCount", listOf())) + .flatMap(JsonRpcResponse::requireStringResult) + .map(Integer::decode) + .timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers"))) + .map { count -> + val minPeers = options.minPeers ?: 1 + if (count < minPeers) { + UpstreamAvailability.IMMATURE + } else { + UpstreamAvailability.OK + } } - } + } } - .single() .onErrorReturn(UpstreamAvailability.UNAVAILABLE) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWs.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWs.kt deleted file mode 100644 index 1995431e..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWs.kt +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2020 EmeraldPay, Inc - * Copyright (c) 2019 ETCDEV GmbH - * - * 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 com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.Defaults -import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.cache.CachesEnabled -import io.emeraldpay.dshackle.config.AuthConfig -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.infinitape.etherjar.rpc.Commands -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson -import io.infinitape.etherjar.rpc.ws.WebsocketClient -import org.slf4j.LoggerFactory -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.core.publisher.TopicProcessor -import reactor.retry.Repeat -import java.net.URI -import java.time.Duration - -class EthereumWs( - private val uri: URI, - private val origin: URI, - private val api: EthereumApi, - private val objectMapper: ObjectMapper -): CachesEnabled { - - private val log = LoggerFactory.getLogger(EthereumWs::class.java) - private val topic = TopicProcessor - .builder() - .name("new-blocks") - .build() - var basicAuth: AuthConfig.ClientBasicAuth? = null - - private var blockCache: Reader = EmptyReader() - - fun connect() { - log.info("Connecting to WebSocket: $uri") - val clientBuilder = WebsocketClient.newBuilder() - .connectTo(uri) - .origin(origin) - basicAuth?.let { auth -> - clientBuilder.basicAuth(auth.username, auth.password) - } - val client = clientBuilder.build() - try { - client.connect() - client.onNewBlock(this::onNewBlock) - } catch (e: Exception) { - log.error("Failed to connect to websocket at $uri. Error: ${e.message}") - } - } - - fun onNewBlock(block: BlockJson) { - // WS returns incomplete blocks - if (block.difficulty == null || block.transactions == null) { - Mono.just(block.hash).flatMap { hash -> - val hash = BlockId.from(hash) - // first check in cache, if empty then check api - blockCache.read(hash) - .switchIfEmpty(request(hash)) - }.repeatWhenEmpty { n -> - Repeat.times(10) - .exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(250)) - .apply(n) - } - .timeout(Defaults.timeout, Mono.empty()) - .subscribe(topic::onNext) - - } else { - topic.onNext(BlockContainer.from(block, objectMapper)) - } - } - - fun request(hash: BlockId): Mono { - return api - .executeAndConvert(Commands.eth().getBlock(io.infinitape.etherjar.domain.BlockHash(hash.value))) - .map { BlockContainer.from(it, objectMapper) } - } - - fun getFlux(): Flux { - return Flux.from(this.topic) - .onBackpressureLatest() - } - - override fun setCaches(caches: Caches) { - blockCache = caches.getBlocksByHash() - } -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsFactory.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsFactory.kt new file mode 100644 index 00000000..8a54be99 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsFactory.kt @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2020 EmeraldPay, Inc + * Copyright (c) 2019 ETCDEV GmbH + * + * 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 com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.Defaults +import io.emeraldpay.dshackle.SilentException +import io.emeraldpay.dshackle.config.AuthConfig +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.infinitape.etherjar.rpc.json.BlockJson +import io.infinitape.etherjar.rpc.json.TransactionRefJson +import io.infinitape.etherjar.rpc.ws.WebsocketClient +import org.slf4j.LoggerFactory +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.publisher.TopicProcessor +import reactor.retry.Repeat +import java.net.URI +import java.time.Duration + +class EthereumWsFactory( + private val uri: URI, + private val origin: URI, + private val objectMapper: ObjectMapper +) { + + var basicAuth: AuthConfig.ClientBasicAuth? = null + + fun create(upstream: EthereumUpstream): EthereumWs { + return EthereumWs(uri, origin, upstream, objectMapper, basicAuth) + } + + class EthereumWs( + private val uri: URI, + private val origin: URI, + private val upstream: EthereumUpstream, + private val objectMapper: ObjectMapper, + private val basicAuth: AuthConfig.ClientBasicAuth? + ) { + + companion object { + private val log = LoggerFactory.getLogger(EthereumWs::class.java) + } + + private val topic = TopicProcessor + .builder() + .name("new-blocks") + .build() + + fun connect() { + log.info("Connecting to WebSocket: $uri") + val clientBuilder = WebsocketClient.newBuilder() + .connectTo(uri) + .origin(origin) + basicAuth?.let { auth -> + clientBuilder.basicAuth(auth.username, auth.password) + } + val client = clientBuilder.build() + try { + client.connect() + client.onNewBlock(this::onNewBlock) + } catch (e: Exception) { + log.error("Failed to connect to websocket at $uri. Error: ${e.message}") + } + } + + fun onNewBlock(block: BlockJson) { + // WS returns incomplete blocks + if (block.difficulty == null || block.transactions == null) { + Mono.just(block.hash) + .flatMap { hash -> + upstream.getApi() + .read(JsonRpcRequest("eth_getBlockByHash", listOf(hash.toHex(), false))) + .flatMap { resp -> + if (resp.isNull()) { + Mono.error(SilentException("Received null for block $hash")) + } else { + Mono.just(resp) + } + } + .flatMap(JsonRpcResponse::requireResult) + .map { BlockContainer.from(it, objectMapper) } + }.repeatWhenEmpty { n -> + Repeat.times(5) + .exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500)) + .apply(n) + } + .timeout(Defaults.timeout, Mono.empty()) + .onErrorResume { Mono.empty() } + .subscribe(topic::onNext) + + } else { + topic.onNext(BlockContainer.from(block, objectMapper)) + } + } + + fun getFlux(): Flux { + return Flux.from(this.topic) + .onBackpressureLatest() + } + } + + + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHead.kt index cb270656..90a5410b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHead.kt @@ -16,15 +16,13 @@ */ package io.emeraldpay.dshackle.upstream.ethereum -import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.cache.CachesEnabled import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable class EthereumWsHead( - private val ws: EthereumWs -): DefaultEthereumHead(), Lifecycle, CachesEnabled { + private val ws: EthereumWsFactory.EthereumWs +) : DefaultEthereumHead(), Lifecycle { private val log = LoggerFactory.getLogger(EthereumWsHead::class.java) @@ -43,8 +41,4 @@ class EthereumWsHead( subscription = null } - override fun setCaches(caches: Caches) { - ws.setCaches(caches) - } - } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouter.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouter.kt new file mode 100644 index 00000000..52d3f2f7 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouter.kt @@ -0,0 +1,129 @@ +/** + * 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 com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.data.TxId +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.infinitape.etherjar.hex.HexQuantity +import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.RpcResponseError +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono +import java.math.BigInteger + +class NativeCallRouter( + private val objectMapper: ObjectMapper, + private val reader: EthereumReader, + private val methods: CallMethods +) : Reader { + + companion object { + private val log = LoggerFactory.getLogger(NativeCallRouter::class.java) + } + + private val fullBlocksReader = EthereumFullBlocksReader( + objectMapper, + reader.blocksByIdAsCont(), + reader.txByHashAsCont() + ) + + override fun read(key: JsonRpcRequest): Mono { + if (methods.isHardcoded(key.method)) { + return Mono.just(methods.executeHardcoded(key.method)) + .map { JsonRpcResponse(it, null) } + } + if (!methods.isAllowed(key.method)) { + return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method")) + } + val common = commonRequests(key) + if (common != null) { + return common.map { JsonRpcResponse(it, null) } + } + return Mono.empty() + } + + /** + * Prepare RpcCall with data types specific for that particular requests. In general it may return a call that just + * parses JSON into Map. But the purpose of further processing and caching for some of the requests we want + * to have actual data types. + */ + fun commonRequests(key: JsonRpcRequest): Mono? { + val method = key.method + val params = key.params + return when { + method == "eth_getTransactionByHash" -> { + if (params.size != 1) { + throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 1 parameter") + } + val hash: TxId + try { + hash = TxId.from(params[0].toString()) + } catch (e: IllegalArgumentException) { + throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be transaction id") + } + reader.txByHashAsCont().read(hash).map { it.json!! } + } + method == "eth_getBlockByHash" -> { + if (params.size != 2) { + throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters") + } + val hash: BlockId + try { + hash = BlockId.from(params[0].toString()) + } catch (e: IllegalArgumentException) { + throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block hash") + } + val withTx = params[1].toString().toBoolean() + if (withTx) { + fullBlocksReader.read(hash).map { it.json!! } + } else { + reader.blocksByIdAsCont().read(hash).map { it.json!! } + } + } + 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() + number = quantity.value.let { + if (it < BigInteger.valueOf(Long.MAX_VALUE) && it >= BigInteger.ZERO) { + it.toLong() + } else { + 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!! } + } + } + else -> null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt index fa16dbaf..18bd641d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt @@ -22,22 +22,21 @@ import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.ReactorBlockchainGrpc import io.emeraldpay.dshackle.Defaults -import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods -import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi +import io.emeraldpay.dshackle.upstream.ethereum.* +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient +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.* -import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable @@ -58,16 +57,15 @@ open class EthereumGrpcUpstream( private val chain: Chain, private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub, private val objectMapper: ObjectMapper, - private val rpcClient: ReactorEmeraldClient -) : DefaultUpstream( + private val client: JsonRpcGrpcClient +) : DefaultUpstream( "$parentId/${chain.chainCode}", UpstreamsConfig.Options.getDefaults(), null -), CachesEnabled, Lifecycle { +), Lifecycle { private var allLabels: Collection = ArrayList() private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java) - private var caches: Caches? = null private val nodes = AtomicReference(QuorumForLabels()) private val head = DefaultEthereumHead() @@ -76,16 +74,7 @@ open class EthereumGrpcUpstream( var timeout = Defaults.timeout - open fun createApi(matcher: Selector.Matcher): DirectEthereumApi { - val targets = this.getMethods() - val client = Selector.extractLabels(matcher)?.let { selector -> - rpcClient.copyWithSelector(selector.asProto()) - } ?: rpcClient - return DirectEthereumApi(client, caches, objectMapper, targets).let { - it.upstream = this - it - } - } + private val defaultReader: Reader = client.forSelector(Selector.empty) override fun start() { if (this.isRunning) return @@ -123,6 +112,7 @@ open class EthereumGrpcUpstream( BigInteger(1, value.weight.toByteArray()), Instant.ofEpochMilli(value.timestamp), false, + null, null ) block @@ -132,9 +122,11 @@ open class EthereumGrpcUpstream( val curr = head.getCurrent() curr == null || curr.difficulty < block.difficulty }.flatMap { - getApi(Selector.EmptyMatcher()) - .flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(BlockHash(it.hash.value))) } - .map { BlockContainer.from(it, objectMapper) } + defaultReader.read(JsonRpcRequest("eth_getBlockByHash", listOf(it.hash.toHexWithPrefix(), false))) + .flatMap(JsonRpcResponse::requireResult) + .map { + BlockContainer.from(it, objectMapper) + } .timeout(timeout, Mono.error(TimeoutException("Timeout from upstream"))) .doOnError { t -> setStatus(UpstreamAvailability.UNAVAILABLE) @@ -208,26 +200,16 @@ open class EthereumGrpcUpstream( return head } - override fun getApi(matcher: Selector.Matcher): Mono { - return Mono.just(createApi(matcher)) - } - - override fun setCaches(caches: Caches) { - this.caches = caches + override fun getApi(): Reader { + return defaultReader } @SuppressWarnings("unchecked") - override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { + override fun cast(selfType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - return castApi(apiType) as T + return this as T } - override fun castApi(apiType: Class): Upstream { - if (!apiType.isAssignableFrom(EthereumApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") - } - return this as Upstream - } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt index 07d45b9d..4e0e3934 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt @@ -25,10 +25,10 @@ import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.startup.UpstreamChange +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient import io.emeraldpay.grpc.Chain import io.grpc.ManagedChannelBuilder import io.grpc.netty.NettyChannelBuilder -import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient import io.netty.handler.ssl.* import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.exception.ExceptionUtils @@ -57,7 +57,6 @@ class GrpcUpstreams( private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null private val known = HashMap() private val lock = ReentrantLock() - private var grpcTransport: ReactorEmeraldClient? = null fun start(): Flux { val channel: ManagedChannelBuilder<*> = if (auth != null && StringUtils.isNotEmpty(auth.ca)) { @@ -74,10 +73,6 @@ class GrpcUpstreams( val client = ReactorBlockchainGrpc.newReactorStub(channel.build()) this.client = client - this.grpcTransport = ReactorEmeraldClient.newBuilder() - .connectUsing(client.channel) - .objectMapper(objectMapper) - .build() val statusSubscription = AtomicReference() @@ -162,7 +157,8 @@ class GrpcUpstreams( lock.withLock { val current = known[chain] return if (current == null) { - val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain)) + val rpcClient = JsonRpcGrpcClient(client!!, chain, objectMapper) + val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, rpcClient) created.timeout = this.timeout known[chain] = created created.start() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcGrpcClient.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcGrpcClient.kt new file mode 100644 index 00000000..a2ba1fe2 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcGrpcClient.kt @@ -0,0 +1,86 @@ +/** + * 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.rpcclient + +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.protobuf.ByteString +import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.api.proto.ReactorBlockchainGrpc +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.grpc.Chain +import io.grpc.Channel +import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.RpcResponseError +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono + +class JsonRpcGrpcClient( + private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub, + private val chain: Chain, + private val objectMapper: ObjectMapper +) { + + companion object { + private val log = LoggerFactory.getLogger(JsonRpcGrpcClient::class.java) + } + + fun forSelector(matcher: Selector.Matcher): Reader { + return Executor(stub, chain, matcher, objectMapper) + } + + class Executor( + private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub, + private val chain: Chain, + private val matcher: Selector.Matcher, + private val objectMapper: ObjectMapper + ) : Reader { + + private val parser = JsonRpcParser() + + override fun read(key: JsonRpcRequest): Mono { + val req = BlockchainOuterClass.NativeCallRequest.newBuilder() + .setChainValue(chain.id) + + if (matcher != Selector.empty) { + Selector.extractLabels(matcher)?.asProto().let { + req.setSelector(it) + } + } + + BlockchainOuterClass.NativeCallItem.newBuilder() + .setId(1) + .setMethod(key.method) + .setPayload(ByteString.copyFrom(objectMapper.writeValueAsBytes(key.params))) + .build().let { + req.addItems(it) + } + + return stub.nativeCall(req.build()) + .single() + .flatMap { resp -> + if (resp.succeed) { + val bytes = resp.payload.toByteArray() + Mono.just(JsonRpcResponse(bytes, null)) + } else { + Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, resp.errorMessage)) + } + } + } + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClient.kt similarity index 51% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClient.kt index 16161307..22e2f843 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClient.kt @@ -13,27 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream.bitcoin +package io.emeraldpay.dshackle.upstream.rpcclient +import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.config.AuthConfig +import io.emeraldpay.dshackle.reader.Reader import io.netty.buffer.Unpooled import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaders +import io.netty.handler.ssl.SslContextBuilder import org.slf4j.LoggerFactory import reactor.core.publisher.Mono import reactor.netty.http.client.HttpClient +import reactor.netty.tcp.SslProvider +import java.io.ByteArrayInputStream +import java.security.KeyStore +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate import java.util.* import java.util.function.Consumer -open class BitcoinRpcClient( +/** + * JSON RPC client + */ +class JsonRpcHttpClient( private val target: String, - basicAuth: AuthConfig.ClientBasicAuth? -) { + private val objectMapper: ObjectMapper, + basicAuth: AuthConfig.ClientBasicAuth? = null, + tlsCAAuth: ByteArray? = null +) : Reader { companion object { - private val log = LoggerFactory.getLogger(BitcoinRpcClient::class.java) + private val log = LoggerFactory.getLogger(JsonRpcHttpClient::class.java) } + private val parser = JsonRpcParser() private val httpClient: HttpClient init { @@ -43,14 +57,27 @@ open class BitcoinRpcClient( h.add(HttpHeaderNames.CONTENT_TYPE, "application/json") } - basicAuth?.let { basicAuth -> - val authString: String = basicAuth.username + ":" + basicAuth.password + basicAuth?.let { auth -> + val authString: String = auth.username + ":" + auth.password val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray()) - val auth = "Basic $authBase64" - val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, auth) } + val encodedAuth = "Basic $authBase64" + val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, encodedAuth) } build = build.headers(headers) } + tlsCAAuth?.let { auth -> + val cf = CertificateFactory.getInstance("X.509") + val cert = cf.generateCertificate(ByteArrayInputStream(auth)) as X509Certificate + val ks = KeyStore.getInstance(KeyStore.getDefaultType()) + ks.load(null, "".toCharArray()) + ks.setCertificateEntry("server", cert) + val sslContext = SslContextBuilder.forClient().trustManager(cert).build() + + build.secure { spec -> + spec.sslContext(sslContext) + } + } + this.httpClient = build } @@ -65,5 +92,10 @@ open class BitcoinRpcClient( .asByteArray() } - + override fun read(key: JsonRpcRequest): Mono { + return Mono.just(key) + .map { it.toJson(objectMapper) } + .flatMap(this@JsonRpcHttpClient::execute) + .map(parser::parse) + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParser.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParser.kt new file mode 100644 index 00000000..6d043ac7 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParser.kt @@ -0,0 +1,98 @@ +/** + * 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.rpcclient + +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import io.infinitape.etherjar.rpc.RpcResponseError +import org.slf4j.LoggerFactory + +class JsonRpcParser() { + + companion object { + private val log = LoggerFactory.getLogger(JsonRpcParser::class.java) + } + + private val jsonFactory = JsonFactory() + + fun parse(json: ByteArray): JsonRpcResponse { + val parser: JsonParser = jsonFactory.createParser(json) + parser.nextToken() + if (parser.currentToken != JsonToken.START_OBJECT) { + return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON")) + } + var nullResponse: JsonRpcResponse? = null + while (parser.nextToken() != JsonToken.END_OBJECT) { + val field = parser.currentName + if (field == "jsonrpc" || field == "id") { + if (!parser.nextToken().isScalarValue) { + return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id/type)")) + } + // just skip the field + } else if (field == "result") { + val value = parser.nextToken() + val start = parser.tokenLocation + if (value.isScalarValue) { + val text = parser.text + if (value == JsonToken.VALUE_STRING) { + return JsonRpcResponse(("\"" + text + "\"").toByteArray(), null) + } else if (value == JsonToken.VALUE_NULL) { + //if null we should check if error is present + nullResponse = JsonRpcResponse(text.toByteArray(), null) + } else { + return JsonRpcResponse(text.toByteArray(), null) + } + } else if (value == JsonToken.START_OBJECT || value == JsonToken.START_ARRAY) { + parser.skipChildren() + val end = parser.currentLocation.byteOffset.toInt() + val copy = ByteArray((end - start.byteOffset).toInt()) + System.arraycopy(json, start.byteOffset.toInt(), copy, 0, copy.size) + return JsonRpcResponse(copy, null) + } + } else if (field == "error") { + val err = readError(parser) + if (err != null) { + return JsonRpcResponse(null, err) + } + } + } + if (nullResponse != null) { + return nullResponse + } + return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure")) + } + + fun readError(parser: JsonParser): JsonRpcResponse.ResponseError? { + var code = 0 + var message = "" + + while (parser.nextToken() != JsonToken.END_OBJECT) { + if (parser.currentToken() == JsonToken.VALUE_NULL) { + // error is just null + return null + } + val field = parser.currentName() + if (field == "code" && parser.currentToken == JsonToken.VALUE_NUMBER_INT) { + code = parser.intValue + } else if (field == "message" && parser.currentToken == JsonToken.VALUE_STRING) { + message = parser.valueAsString + } + } + + return JsonRpcResponse.ResponseError(code, message) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt new file mode 100644 index 00000000..d8fd2d83 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt @@ -0,0 +1,52 @@ +/** + * 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.rpcclient + +import com.fasterxml.jackson.databind.ObjectMapper + +class JsonRpcRequest( + val method: String, + val params: List +) { + + fun toJson(objectMapper: ObjectMapper): ByteArray { + val json = mapOf( + "jsonrpc" to "2.0", + "id" to 1, + "method" to method, + "params" to params + ) + return objectMapper.writeValueAsBytes(json) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JsonRpcRequest) return false + + if (method != other.method) return false + if (params != other.params) return false + + return true + } + + override fun hashCode(): Int { + var result = method.hashCode() + result = 31 * result + params.hashCode() + return result + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponse.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponse.kt new file mode 100644 index 00000000..218e3304 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponse.kt @@ -0,0 +1,136 @@ +/** + * 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.rpcclient + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import io.infinitape.etherjar.rpc.RpcException +import reactor.core.publisher.Mono + +class JsonRpcResponse( + private val result: ByteArray?, + val error: ResponseError? +) { + + companion object { + private val NULL_VALUE = "null".toByteArray() + + @JvmStatic + fun ok(value: ByteArray): JsonRpcResponse { + return JsonRpcResponse(value, null) + } + + @JvmStatic + fun ok(value: String): JsonRpcResponse { + return JsonRpcResponse(value.toByteArray(), null) + } + + @JvmStatic + fun error(code: Int, msg: String): JsonRpcResponse { + return JsonRpcResponse(null, ResponseError(code, msg)) + } + } + + fun hasResult(): Boolean { + return result != null + } + + fun hasError(): Boolean { + return error != null + } + + fun isNull(): Boolean { + return result != null && NULL_VALUE.contentEquals(result) + } + + fun getResult(): ByteArray { + return result ?: ByteArray(0) + } + + fun getResultAsRawString(): String { + return String(getResult()) + } + + fun getResultAsProcessedString(): String { + val str = getResultAsRawString() + if (str.startsWith("\"") && str.endsWith("\"")) { + return str.substring(1, str.length - 1) + } + throw IllegalStateException("Not as JS string") + } + + fun requireResult(): Mono { + return if (error != null) { + Mono.error(error.asException()) + } else { + Mono.just(getResult()) + } + } + + fun requireStringResult(): Mono { + return if (error != null) { + Mono.error(error.asException()) + } else { + Mono.just(getResultAsProcessedString()) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JsonRpcResponse) return false + + if (result != null) { + if (other.result == null) return false + if (!result.contentEquals(other.result)) return false + } else if (other.result != null) return false + if (error != other.error) return false + + return true + } + + override fun hashCode(): Int { + var result1 = result?.contentHashCode() ?: 0 + result1 = 31 * result1 + (error?.hashCode() ?: 0) + return result1 + } + + class ResponseError(val code: Int, val message: String) { + fun asException(): RpcException { + return RpcException(code, message) + } + } + + class ResponseJsonSerializer : JsonSerializer() { + override fun serialize(value: JsonRpcResponse, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeStartObject() + gen.writeStringField("jsonrpc", "2.0") + gen.writeNumberField("id", 0) + if (value.error != null) { + gen.writeObjectFieldStart("error") + gen.writeNumberField("code", value.error.code) + gen.writeStringField("message", value.error.message) + gen.writeEndObject() + } else { + if (value.result == null) { + throw IllegalStateException("No result set") + } + gen.writeRawUTF8String(value.result, 0, value.result.size) + } + gen.writeEndObject() + } + } +} \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy index 2260872b..cd01f207 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy @@ -63,6 +63,7 @@ class BlocksRedisCacheSpec extends Specification { Instant.ofEpochSecond(10501050), false, "test".bytes, + null, [TxId.from(hash2), TxId.from(hash1)] ) diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy index 70218411..bcc6375a 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy @@ -63,7 +63,8 @@ class TxRedisCacheSpec extends Specification { 2000, TxId.from(hash1), BlockId.from(hash2), - "test".bytes + "test".bytes, + null ) when: def enc = cache.toProto(cont) diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy index 7b4ec70e..427ed8a4 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy @@ -20,16 +20,16 @@ import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.quorum.BroadcastQuorum +import io.infinitape.etherjar.rpc.RpcException import spock.lang.Specification class BroadcastQuorumSpec extends Specification { - def rpcConverted = TestingCommons.rpcConverter() def objectMapper = TestingCommons.objectMapper() def "Resolved with first after 3 tries"() { setup: - def q = Spy(new BroadcastQuorum(rpcConverted, 3)) + def q = Spy(new BroadcastQuorum(objectMapper, 3)) def upstream1 = Stub(Upstream) def upstream2 = Stub(Upstream) def upstream3 = Stub(Upstream) @@ -40,28 +40,28 @@ class BroadcastQuorumSpec extends Specification { !q.isResolved() when: - q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream1) + q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream1) then: !q.isResolved() 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) when: - q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2) + q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2) then: !q.isResolved() 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) when: - q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3) + q.record(new RpcException(1, "Nonce too low"), upstream3) then: 1 * q.recordError(_, _, _) q.isResolved() - objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"] + objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c" } def "Remembers first response"() { setup: - def q = Spy(new BroadcastQuorum(rpcConverted, 3)) + def q = Spy(new BroadcastQuorum(objectMapper, 3)) def upstream1 = Stub(Upstream) def upstream2 = Stub(Upstream) def upstream3 = Stub(Upstream) @@ -72,22 +72,22 @@ class BroadcastQuorumSpec extends Specification { !q.isResolved() when: - q.record(objectMapper.writeValueAsBytes([error: [message: "Internal error"]]), upstream1) + q.record(new RpcException(1, "Internal error"), upstream1) then: !q.isResolved() 1 * q.recordError(_, _, _) when: - q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2) + q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2) then: !q.isResolved() 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) when: - q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3) + q.record(new RpcException(1, "Nonce too low"), upstream3) then: 1 * q.recordError(_, _, _) q.isResolved() - objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"] + objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c" } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/NonEmptyQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonEmptyQuorumSpec.groovy new file mode 100644 index 00000000..3f38278e --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonEmptyQuorumSpec.groovy @@ -0,0 +1,133 @@ +/** + * 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.quorum + +import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Upstream +import io.infinitape.etherjar.rpc.RpcException +import spock.lang.Specification + +class NonEmptyQuorumSpec extends Specification { + + def "Fail if too many errors"() { + setup: + def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + def upstream1 = Stub(Upstream) + def upstream2 = Stub(Upstream) + def upstream3 = Stub(Upstream) + + when: + q.init(Stub(Head)) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record(new RpcException(1, "Internal"), upstream1) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record(new RpcException(1, "Internal"), upstream2) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record(new RpcException(1, "Internal"), upstream3) + then: + q.isFailed() + !q.isResolved() + } + + def "Fail first if not error"() { + setup: + def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + def upstream1 = Stub(Upstream) + def upstream2 = Stub(Upstream) + def upstream3 = Stub(Upstream) + + when: + q.init(Stub(Head)) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record('"0x11"'.bytes, upstream1) + then: + q.isResolved() + !q.isFailed() + } + + def "Fail second if first is error"() { + setup: + def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + def upstream1 = Stub(Upstream) + def upstream2 = Stub(Upstream) + def upstream3 = Stub(Upstream) + + when: + q.init(Stub(Head)) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record(new RpcException(1, "Internal"), upstream1) + then: + !q.isFailed() + !q.isResolved() + + + when: + q.record('"0x11"'.bytes, upstream2) + then: + q.isResolved() + !q.isFailed() + } + + def "Fail second if first is null"() { + setup: + def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + def upstream1 = Stub(Upstream) + def upstream2 = Stub(Upstream) + def upstream3 = Stub(Upstream) + + when: + q.init(Stub(Head)) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record('null'.bytes, upstream2) + then: + !q.isFailed() + !q.isResolved() + + + when: + q.record('"0x11"'.bytes, upstream2) + then: + q.isResolved() + !q.isFailed() + } + + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy index 89e67407..83cededc 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy @@ -20,16 +20,16 @@ import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.quorum.NonceQuorum +import io.infinitape.etherjar.rpc.RpcException import spock.lang.Specification class NonceQuorumSpec extends Specification { - def rpcConverted = TestingCommons.rpcConverter() def objectMapper = TestingCommons.objectMapper() def "Gets max value"() { setup: - def q = Spy(new NonceQuorum(rpcConverted, 3)) + def q = Spy(new NonceQuorum(objectMapper, 3)) def upstream1 = Stub(Upstream) def upstream2 = Stub(Upstream) def upstream3 = Stub(Upstream) @@ -40,28 +40,28 @@ class NonceQuorumSpec extends Specification { !q.isResolved() when: - q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream1) + q.record('"0x10"'.bytes, upstream1) then: !q.isResolved() 1 * q.recordValue(_, "0x10", _) when: - q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2) + q.record('"0x11"'.bytes, upstream2) then: !q.isResolved() 1 * q.recordValue(_, "0x11", _) when: - q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3) + q.record('"0x10"'.bytes, upstream3) then: 1 * q.recordValue(_, "0x10", _) q.isResolved() - objectMapper.readValue(q.result, Map) == [result: "0x11"] + objectMapper.readValue(q.result, Object) == "0x11" } def "Ignores errors"() { setup: - def q = Spy(new NonceQuorum(rpcConverted, 3)) + def q = Spy(new NonceQuorum(objectMapper, 3)) def upstream1 = Stub(Upstream) def upstream2 = Stub(Upstream) def upstream3 = Stub(Upstream) @@ -72,28 +72,60 @@ class NonceQuorumSpec extends Specification { !q.isResolved() when: - q.record(objectMapper.writeValueAsBytes([error: [error: "Internal"]]), upstream1) + q.record(new RpcException(1, "Internal"), upstream1) then: !q.isResolved() 1 * q.recordError(_, _, _) when: - q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2) + q.record('"0x11"'.bytes, upstream2) then: !q.isResolved() 1 * q.recordValue(_, "0x11", _) when: - q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3) + q.record('"0x10"'.bytes, upstream3) then: 1 * q.recordValue(_, "0x10", _) !q.isResolved() when: - q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream1) + q.record('"0x11"'.bytes, upstream1) then: 1 * q.recordValue(_, "0x11", _) q.isResolved() - objectMapper.readValue(q.result, Map) == [result: "0x11"] + objectMapper.readValue(q.result, Object) == "0x11" + } + + def "Fail if too many errors"() { + setup: + def q = Spy(new NonceQuorum(objectMapper, 3)) + def upstream1 = Stub(Upstream) + def upstream2 = Stub(Upstream) + def upstream3 = Stub(Upstream) + + when: + q.init(Stub(Head)) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record(new RpcException(1, "Internal"), upstream1) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record(new RpcException(1, "Internal"), upstream2) + then: + !q.isResolved() + !q.isFailed() + + when: + q.record(new RpcException(1, "Internal"), upstream3) + then: + q.isFailed() + !q.isResolved() } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/QuorumRpcReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/QuorumRpcReaderSpec.groovy new file mode 100644 index 00000000..0af237c2 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/QuorumRpcReaderSpec.groovy @@ -0,0 +1,206 @@ +/** + * 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.quorum + +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.FilteredApis +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration + +class QuorumRpcReaderSpec extends Specification { + + def "always-quorum - get the result if ok"() { + setup: + def up = Mock(Upstream) { + _ * isAvailable() >> true + 1 * getApi() >> Mock(Reader) { + 1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(JsonRpcResponse.ok("1")) + } + } + def apis = new FilteredApis( + [up], Selector.empty + ) + def reader = new QuorumRpcReader(apis, new AlwaysQuorum()) + + when: + def act = reader.read(new JsonRpcRequest("eth_test", [])) + .map { + new String(it.value) + } + + then: + StepVerifier.create(act) + .expectNext("1") + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "always-quorum - retry upstream error"() { + setup: + def up = Mock(Upstream) { + _ * isAvailable() >> true + _ * getApi() >> Mock(Reader) { + 2 * read(new JsonRpcRequest("eth_test", [])) >>> [ + Mono.just(JsonRpcResponse.error(1, "test")), + Mono.just(JsonRpcResponse.ok("1")) + ] + } + } + def apis = new FilteredApis( + [up], Selector.empty + ) + def reader = new QuorumRpcReader(apis, new AlwaysQuorum()) + + when: + def act = reader.read(new JsonRpcRequest("eth_test", [])) + .map { + new String(it.value) + } + + then: + StepVerifier.create(act) + .expectNext("1") + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "non-empty-quorum - get the second result if first is null"() { + setup: + def up = Mock(Upstream) { + _ * isAvailable() >> true + _ * getApi() >> Mock(Reader) { + 2 * read(new JsonRpcRequest("eth_test", [])) >>> [ + Mono.just(JsonRpcResponse.ok("null")), + Mono.just(JsonRpcResponse.ok("1")) + ] + } + } + def apis = new FilteredApis( + [up], Selector.empty + ) + def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + + when: + def act = reader.read(new JsonRpcRequest("eth_test", [])) + .map { + new String(it.value) + } + + then: + StepVerifier.create(act) + .expectNext("1") + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + + def "non-empty-quorum - get the second result if first is error"() { + setup: + def up = Mock(Upstream) { + _ * isAvailable() >> true + _ * getApi() >> Mock(Reader) { + 2 * read(new JsonRpcRequest("eth_test", [])) >>> [ + Mono.just(JsonRpcResponse.error(1, "test")), + Mono.just(JsonRpcResponse.ok("1")) + ] + } + } + def apis = new FilteredApis( + [up], Selector.empty + ) + def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + + when: + def act = reader.read(new JsonRpcRequest("eth_test", [])) + .map { + new String(it.value) + } + + then: + StepVerifier.create(act) + .expectNext("1") + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "non-empty-quorum - get the third result if first two are not ok"() { + setup: + def up = Mock(Upstream) { + _ * isAvailable() >> true + _ * getApi() >> Mock(Reader) { + 3 * read(new JsonRpcRequest("eth_test", [])) >>> [ + Mono.just(JsonRpcResponse.ok("null")), + Mono.just(JsonRpcResponse.error(1, "test")), + Mono.just(JsonRpcResponse.ok("1")) + ] + } + } + def apis = new FilteredApis( + [up], Selector.empty + ) + def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + + when: + def act = reader.read(new JsonRpcRequest("eth_test", [])) + .map { + new String(it.value) + } + + then: + StepVerifier.create(act) + .expectNext("1") + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "non-empty-quorum - no result if all failed"() { + setup: + def up = Mock(Upstream) { + _ * isAvailable() >> true + _ * getApi() >> Mock(Reader) { + 3 * read(new JsonRpcRequest("eth_test", [])) >>> [ + Mono.just(JsonRpcResponse.ok("null")), + Mono.just(JsonRpcResponse.error(1, "test")), + Mono.just(JsonRpcResponse.ok("null")) + ] + } + } + def apis = new FilteredApis( + [up], Selector.empty + ) + def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3)) + + when: + def act = reader.read(new JsonRpcRequest("eth_test", [])) + .map { + new String(it.value) + } + + then: + StepVerifier.create(act) + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/ValueAwareQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/ValueAwareQuorumSpec.groovy new file mode 100644 index 00000000..f985b0fc --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/ValueAwareQuorumSpec.groovy @@ -0,0 +1,98 @@ +/** + * 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.quorum + +import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Upstream +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable +import spock.lang.Specification + +class ValueAwareQuorumSpec extends Specification { + + def "Extract null"() { + setup: + def quorum = new ValueAwareQuorumImpl() + when: + def act = quorum.extractValue("null".bytes, Object) + then: + act == null + } + + def "Extract string"() { + setup: + def quorum = new ValueAwareQuorumImpl() + when: + def act = quorum.extractValue("\"foo\"".bytes, Object) + then: + act == "foo" + } + + def "Extract number"() { + setup: + def quorum = new ValueAwareQuorumImpl() + when: + def act = quorum.extractValue("100".bytes, Object) + then: + act == 100 + } + + def "Extract map"() { + setup: + def quorum = new ValueAwareQuorumImpl() + when: + def act = quorum.extractValue("{\"foo\": 1}".bytes, Object) + then: + act == [foo: 1] + } + + class ValueAwareQuorumImpl extends ValueAwareQuorum { + ValueAwareQuorumImpl() { + super(TestingCommons.objectMapper(), Object) + } + + @Override + void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @NotNull Upstream upstream) { + + } + + @Override + void recordError(@Nullable byte[] response, @Nullable String errorMessage, @NotNull Upstream upstream) { + + } + + @Override + void init(@NotNull Head head) { + + } + + @Override + boolean isResolved() { + return false + } + + @Override + byte[] getResult() { + return new byte[0] + } + + @Override + boolean isFailed() { + return false + } + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy index c1a156b1..d43df755 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy @@ -19,21 +19,25 @@ package io.emeraldpay.dshackle.rpc import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.dshackle.quorum.BroadcastQuorum +import io.emeraldpay.dshackle.quorum.QuorumReaderFactory +import io.emeraldpay.dshackle.quorum.QuorumRpcReader +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.quorum.AlwaysQuorum -import io.emeraldpay.dshackle.upstream.CachingEthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.quorum.NonEmptyQuorum +import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.Selector -import io.emeraldpay.dshackle.upstream.Upstream -import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.ethereum.NativeCallRouter +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.rpc.ReactorRpcClient import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcResponseError -import io.infinitape.etherjar.rpc.RpcResponseException import reactor.core.publisher.Mono import reactor.test.StepVerifier +import spock.lang.Ignore import spock.lang.Specification import java.time.Duration @@ -43,140 +47,99 @@ class NativeCallSpec extends Specification { def objectMapper = TestingCommons.objectMapper() + def "Tries router first"() { + def routedApi = Mock(Reader) { + 1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null)) + } + def upstream = Mock(Multistream) { + 1 * getRoutedApi(_) >> Mono.just(routedApi) + } + def upstreams = Stub(MultistreamHolder) + + def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) + def ctx = new NativeCall.CallContext( + 1, upstream, Selector.empty, new AlwaysQuorum(), + new NativeCall.ParsedCallDetails("eth_test", []) + ) + + when: + def act = nativeCall.fetch(ctx).block(Duration.ofSeconds(1)) + then: + act.payload == "1".bytes + } + + def "Return error if router denied the requests"() { + def routedApi = Mock(Reader) { + 1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message")) + } + def upstream = Mock(Multistream) { + 1 * getRoutedApi(_) >> Mono.just(routedApi) + } + def upstreams = Stub(MultistreamHolder) + + def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) + def ctx = new NativeCall.CallContext( + 15, upstream, Selector.empty, new AlwaysQuorum(), + new NativeCall.ParsedCallDetails("eth_test", []) + ) + + when: + def act = nativeCall.fetch(ctx) //.block(Duration.ofSeconds(1)) + then: + StepVerifier.create(act) + .expectErrorMatches { t -> + t instanceof NativeCall.CallFailure && + t.id == 15 && + t.reason instanceof RpcException && + t.reason.rpcMessage == "Test message" + } + .verify(Duration.ofSeconds(1)) + } + def "Quorum is applied"() { setup: - def quorum = Spy(new AlwaysQuorum()) - def upstreams = Stub(Upstreams) - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - apiMock.upstream = Stub(Upstream) + def quorum = new AlwaysQuorum() - apiMock.answer("eth_test", [], "foo") - - def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), - Selector.empty, quorum, + def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper()) + nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) { + 1 * create(_, _) >> Mock(Reader) { + 1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, 1)) + } + } + def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum, new NativeCall.ParsedCallDetails("eth_test", [])) when: - def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2)) - def act = objectMapper.readValue(resp.payload, Map) + def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1)) + def act = objectMapper.readValue(resp.payload, Object) then: - act == [jsonrpc:"2.0", id:1, result: "foo"] - 1 * quorum.record(_, _) - 1 * quorum.getResult() - } - - def "Quorum may return not first received value"() { - setup: - def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3)) - - def upstreams = Stub(Upstreams) - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - apiMock.upstream = Stub(Upstream) - - apiMock.answerOnce("eth_test", [], null) - apiMock.answerOnce("eth_test", [], "bar") - apiMock.answerOnce("eth_test", [], null) - - def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), - Selector.empty, quorum, - new NativeCall.ParsedCallDetails("eth_test", [])) - - - when: - def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2)) - def act = objectMapper.readValue(resp.payload, Map) - then: - act == [jsonrpc:"2.0", id:1, result: "bar"] - 2 * quorum.record(_, _) - 1 * quorum.getResult() - } - - def "Have pause between repeats"() { - setup: - def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3)) - - def upstreams = Stub(Upstreams) - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - apiMock.upstream = Stub(Upstream) - - apiMock.answerOnce("eth_test", [], null) - apiMock.answerOnce("eth_test", [], "bar") - - def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), - Selector.empty, quorum, - new NativeCall.ParsedCallDetails("eth_test", [])) - - - when: - def t1 = System.currentTimeMillis() - def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2)) - def delta = System.currentTimeMillis() - t1 - then: - delta > 95 // should be 100, but sometimes gives less ??? - new String(resp.payload) == '{"jsonrpc":"2.0","id":1,"result":"bar"}' - } - - def "One call has no pause"() { - setup: - def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3)) - - def upstreams = Stub(Upstreams) - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - apiMock.upstream = Stub(Upstream) - - apiMock.answerOnce("eth_test", [], "bar") - - def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), - Selector.empty, quorum, - new NativeCall.ParsedCallDetails("eth_test", [])) - - - when: - def t1 = System.currentTimeMillis() - nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2)) - def delta = System.currentTimeMillis() - t1 - then: - delta < 50 + act == "foo" } def "Returns error if no quorum"() { setup: - def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3)) + def quorum = new AlwaysQuorum() - def upstreams = Stub(Upstreams) - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - apiMock.upstream = Stub(Upstream) - - apiMock.answer("eth_test", [], null, 3) - apiMock.answerOnce("eth_test", [], "foo") - - def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), Selector.empty, quorum, + def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper()) + nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) { + 1 * create(_, _) >> Mock(Reader) { + 1 * read(_) >> Mono.empty() + } + } + def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum, new NativeCall.ParsedCallDetails("eth_test", [])) - 3 * quorum.record(_, _) - 1 * quorum.getResult() - when: def resp = nativeCall.executeOnRemote(call) then: StepVerifier.create(resp) - .expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 1}) - .verify(Duration.ofSeconds(1)) + .expectErrorMatches({ t -> t instanceof NativeCall.CallFailure && t.id == 1 }) + .verify(Duration.ofSeconds(1)) } def "Packs call exception into response with id"() { setup: - def upstreams = Stub(Upstreams) + def upstreams = Stub(MultistreamHolder) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) when: def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test"))) @@ -193,7 +156,7 @@ class NativeCallSpec extends Specification { def "Packs unknown exception into response"() { setup: - def upstreams = Stub(Upstreams) + def upstreams = Stub(MultistreamHolder) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) when: def resp = nativeCall.processException(new IllegalArgumentException("test test")) @@ -209,13 +172,13 @@ class NativeCallSpec extends Specification { def "Builds normal response"() { setup: - def upstreams = Stub(Upstreams) + def upstreams = Stub(MultistreamHolder) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def json = [jsonrpc:"2.0", id:1, result: "foo"] when: def resp = nativeCall.buildResponse( - new NativeCall.CallContext(1561, TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi)), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json)) + new NativeCall.CallContext(1561, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json)) ) then: resp.id == 1561 @@ -225,7 +188,7 @@ class NativeCallSpec extends Specification { def "Returns error for invalid chain"() { setup: - def upstreams = Stub(Upstreams) + def upstreams = Stub(MultistreamHolder) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def req = BlockchainOuterClass.NativeCallRequest.newBuilder() @@ -248,7 +211,7 @@ class NativeCallSpec extends Specification { def "Returns error for unsupported chain"() { setup: - def upstreams = Mock(Upstreams) + def upstreams = Mock(MultistreamHolder) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def req = BlockchainOuterClass.NativeCallRequest.newBuilder() @@ -270,14 +233,14 @@ class NativeCallSpec extends Specification { .verify(Duration.ofSeconds(1)) } + @Ignore + //TODO def "Calls cache before remote"() { setup: - def upstreams = Stub(Upstreams) + def upstreams = Stub(MultistreamHolder) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def api = Mock(DirectEthereumApi) + def api = TestingCommons.api() def upstream = TestingCommons.aggregatedUpstream(api) - def cacheMock = Mock(CachingEthereumApi) - upstream.cache = cacheMock def ctx = new NativeCall.CallContext(10, upstream, @@ -289,13 +252,13 @@ class NativeCallSpec extends Specification { 1 * cacheMock.execute(10, "eth_test", []) >> Mono.empty() } + @Ignore + //TODO def "Uses cached value"() { setup: - def upstreams = Stub(Upstreams) + def upstreams = Stub(MultistreamHolder) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def upstream = TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi)) - def cacheMock = Mock(CachingEthereumApi) - upstream.cache = cacheMock + def upstream = TestingCommons.aggregatedUpstream(TestingCommons.api()) def ctx = new NativeCall.CallContext(10, upstream, @@ -307,63 +270,4 @@ class NativeCallSpec extends Specification { 1 * cacheMock.execute(10, "eth_test", []) >> Mono.just('{"result": "foo"}'.bytes) new String(act.block().payload) == '{"result": "foo"}' } - - def "Retries on error"() { - setup: - def quorum = Spy(new AlwaysQuorum()) - - def upstreams = Stub(Upstreams) - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - apiMock.upstream = Stub(Upstream) - - apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 1")) - apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 2")) - apiMock.answerOnce("eth_test", [], "bar") - - def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), - Selector.empty, quorum, - new NativeCall.ParsedCallDetails("eth_test", [])) - - - when: - def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2)) - def act = objectMapper.readValue(resp.payload, Map) - then: - act == [jsonrpc:"2.0", id:1, result: "bar"] - 1 * quorum.record(_, _) - 1 * quorum.getResult() - } - - def "Send raw retries 3 times"() { - setup: - def quorum = Spy(new BroadcastQuorum(TestingCommons.rpcConverter(), 3)) - - def upstreams = Stub(Upstreams) - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - apiMock.upstream = Stub(Upstream) - - apiMock.answer("eth_sendRawTransaction", ["0x1234"], - "0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b", 1) - apiMock.answer("eth_sendRawTransaction", ["0x1234"], null, 10, - new RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported")) -// apiMock.answer("eth_sendRawTransaction", ["0x1234"], -// new RpcResponseError(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported"), 10) - - def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) - def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), - Selector.empty, quorum, - new NativeCall.ParsedCallDetails("eth_sendRawTransaction", ["0x1234"])) - - - when: - def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2)) - def act = objectMapper.readValue(resp.payload, Map) - then: - act == [jsonrpc:"2.0", id:1, result: "0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b"] - 1 * quorum.record(_ as byte[], _) - 2 * quorum.record(_ as RpcException, _) - } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/StreamHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/StreamHeadSpec.groovy index 6fac3820..199629ce 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/StreamHeadSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/StreamHeadSpec.groovy @@ -23,13 +23,10 @@ import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.test.EthereumUpstreamMock import io.emeraldpay.dshackle.test.TestingCommons -import io.emeraldpay.dshackle.test.UpstreamsMock -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.test.MultistreamHolderMock import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.domain.BlockHash -import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.TransactionRefJson import reactor.core.publisher.Mono @@ -45,7 +42,7 @@ class StreamHeadSpec extends Specification { def "Errors on unavailable chain"() { setup: - def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(EthereumUpstream)) + def upstreams = new MultistreamHolderMock(Chain.ETHEREUM, Stub(EthereumUpstream)) def streamHead = new StreamHead(upstreams) when: def flux = streamHead.add( @@ -80,8 +77,8 @@ class StreamHeadSpec extends Specification { .build() } - def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, Stub(DirectEthereumApi.class)) - def upstreams = new UpstreamsMock(Chain.ETHEREUM, upstream) + def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, TestingCommons.api()) + def upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstream) def streamHead = new StreamHead(upstreams) when: def flux = streamHead.add( diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy index f0bed1b0..9f4a0488 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy @@ -20,11 +20,11 @@ import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.test.TestingCommons -import io.emeraldpay.dshackle.upstream.AggregatedUpstream +import io.emeraldpay.dshackle.test.MultistreamHolderMock import io.emeraldpay.dshackle.upstream.Head -import io.emeraldpay.dshackle.upstream.Upstream -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader import io.emeraldpay.grpc.Chain import reactor.core.publisher.Flux import reactor.core.publisher.Mono @@ -43,7 +43,7 @@ class TrackBitcoinAddressSpec extends Specification { setup: def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json") def unspents = TestingCommons.objectMapper().readValue(json, List) - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) when: def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents) @@ -58,7 +58,7 @@ class TrackBitcoinAddressSpec extends Specification { setup: def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") def unspents = TestingCommons.objectMapper().readValue(json, List) - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) when: def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents) @@ -73,7 +73,7 @@ class TrackBitcoinAddressSpec extends Specification { setup: def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") def unspents = TestingCommons.objectMapper().readValue(json, List) - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) when: def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address } @@ -93,7 +93,7 @@ class TrackBitcoinAddressSpec extends Specification { def "Zero for empty unspents"() { setup: - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) when: def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], []) @@ -108,7 +108,7 @@ class TrackBitcoinAddressSpec extends Specification { setup: def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") def unspents = TestingCommons.objectMapper().readValue(json, List) - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) when: def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address } @@ -126,7 +126,7 @@ class TrackBitcoinAddressSpec extends Specification { def "One address for single provided"() { setup: - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) def req = BlockchainOuterClass.BalanceRequest.newBuilder() .setAddress( Common.AnyAddress.newBuilder() @@ -144,7 +144,7 @@ class TrackBitcoinAddressSpec extends Specification { def "Sorted addresses for multiple provided"() { setup: - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) def req = BlockchainOuterClass.BalanceRequest.newBuilder() .setAddress( Common.AnyAddress.newBuilder() @@ -165,7 +165,7 @@ class TrackBitcoinAddressSpec extends Specification { def "Null for no address provided"() { setup: - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) def req = BlockchainOuterClass.BalanceRequest.newBuilder() .build() when: @@ -176,7 +176,7 @@ class TrackBitcoinAddressSpec extends Specification { def "Build proto for common balance"() { setup: - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(123456)) when: def act = track.buildResponse(balance) @@ -189,7 +189,7 @@ class TrackBitcoinAddressSpec extends Specification { def "Build proto for zero balance"() { setup: - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.ZERO) when: def act = track.buildResponse(balance) @@ -202,7 +202,7 @@ class TrackBitcoinAddressSpec extends Specification { def "Build proto for all bitcoins"() { setup: - TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(21_000_000).multiply(BigInteger.TEN.pow(8))) when: def act = track.buildResponse(balance) @@ -216,24 +216,24 @@ class TrackBitcoinAddressSpec extends Specification { def "Get update for a balance"() { setup: - DirectBitcoinApi api = Mock(DirectBitcoinApi) { - 2 * executeAndResult(0, "listunspent", [], List) >>> [ - Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]]) - ] - } def blocks = TopicProcessor.create() Head head = Mock(Head) { 1 * getFlux() >> Flux.from(blocks) } - Upstream upstream - upstream = Mock(AggregatedUpstream) { - _ * getApi(_) >> Mono.just(api) + def upstream = null + upstream = Mock(BitcoinMultistream) { + _ * getReader() >> Mock(BitcoinReader) { + 2 * listUnspent() >>> [ + Mono.just([]), + Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]]) + ] + } _ * getHead() >> head - _ * castApi(_) >> { return upstream } - } - Upstreams upstreams = Mock(Upstreams) { - _ * getUpstream(Chain.BITCOIN) >> upstream + _ * cast(_) >> { + upstream + } } + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.BITCOIN, upstream) TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams) when: @@ -253,7 +253,7 @@ class TrackBitcoinAddressSpec extends Specification { StepVerifier.create(resp) .expectNext("0") .then { - blocks.onNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, [])) + blocks.onNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, null, [])) } .expectNext("1230000") .then { diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy index ed15dfe5..4a04dee5 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy @@ -18,11 +18,9 @@ package io.emeraldpay.dshackle.rpc import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.upstream.Head -import io.emeraldpay.dshackle.upstream.Upstream -import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader -import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.CachingMempoolData import io.emeraldpay.grpc.Chain import reactor.core.publisher.Flux @@ -37,7 +35,7 @@ class TrackBitcoinTxSpec extends Specification { def "loadMempool() returns not found when not found"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) CachingMempoolData mempoolAccess = Mock(CachingMempoolData) { 1 * get() >> Mono.just([ @@ -45,8 +43,8 @@ class TrackBitcoinTxSpec extends Specification { "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" ]) } - BitcoinUpstream upstream = Mock(BitcoinUpstream) { - _ * getData() >> Mock(BitcoinReader) { + BitcoinMultistream upstream = Mock(BitcoinMultistream) { + _ * getReader() >> Mock(BitcoinReader) { _ * getMempool() >> mempoolAccess } } @@ -64,15 +62,15 @@ class TrackBitcoinTxSpec extends Specification { def "loadMempool() returns ok when found"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) CachingMempoolData mempoolAccess = Mock(CachingMempoolData) { 1 * get() >> Mono.just([ "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" ]) } - BitcoinUpstream upstream = Mock(BitcoinUpstream) { - _ * getData() >> Mock(BitcoinReader) { + BitcoinMultistream upstream = Mock(BitcoinMultistream) { + _ * getReader() >> Mock(BitcoinReader) { _ * getMempool() >> mempoolAccess } } @@ -90,15 +88,17 @@ class TrackBitcoinTxSpec extends Specification { def "loadExiting() returns not found if not mined"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - DirectBitcoinApi api = Mock(DirectBitcoinApi) { - 1 * getTx(txid) >> Mono.just([ - txid: txid - ]) + BitcoinMultistream upstream = Mock(BitcoinMultistream) { + _ * getReader() >> Mock(BitcoinReader) { + 1 * getTx(txid) >> Mono.just([ + txid: txid + ]) + } } when: - def act = track.loadExisting(api, txid) + def act = track.loadExisting(upstream, txid) then: StepVerifier.create(act) @@ -111,17 +111,19 @@ class TrackBitcoinTxSpec extends Specification { def "loadExiting() returns block if mined"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - DirectBitcoinApi api = Mock(DirectBitcoinApi) { - 1 * getTx(txid) >> Mono.just([ - txid : txid, - blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", - height : 100 - ]) + BitcoinMultistream upstream = Mock(BitcoinMultistream) { + _ * getReader() >> Mock(BitcoinReader) { + 1 * getTx(txid) >> Mono.just([ + txid : txid, + blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", + height : 100 + ]) + } } when: - def act = track.loadExisting(api, txid) + def act = track.loadExisting(upstream, txid) then: StepVerifier.create(act) @@ -136,16 +138,16 @@ class TrackBitcoinTxSpec extends Specification { def "Goes with confirmations"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" // start with the current block def next = Flux.fromIterable([10, 12, 13, 14, 15]).map { h -> - new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, []) + new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, []) } Head head = Mock(Head) { 1 * getFlux() >> next } - Upstream upstream = Mock(BitcoinUpstream) { + BitcoinMultistream upstream = Mock(BitcoinMultistream) { 1 * getHead() >> head } def status = new TrackBitcoinTx.TxStatus( @@ -167,16 +169,16 @@ class TrackBitcoinTxSpec extends Specification { def "Wait until mined"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" // start with the current block def next = Flux.fromIterable([10, 12, 13]).map { h -> - new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, []) + new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, []) } Head head = Mock(Head) { 1 * getFlux() >> next } - DirectBitcoinApi api = Mock(DirectBitcoinApi) { + BitcoinReader api = Mock(BitcoinReader) { 3 * getTx(txid) >>> [ Mono.just([ txid: txid @@ -191,9 +193,9 @@ class TrackBitcoinTxSpec extends Specification { ]) ] } - Upstream upstream = Mock(BitcoinUpstream) { + BitcoinMultistream upstream = Mock(BitcoinMultistream) { 1 * getHead() >> head - _ * getApi(_) >> Mono.just(api) + _ * getReader() >> api } def status = new TrackBitcoinTx.TxStatus( txid, false, null, false, null, null, null, 0 @@ -210,13 +212,9 @@ class TrackBitcoinTxSpec extends Specification { def "Check mempool until found"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - DirectBitcoinApi api = Mock(DirectBitcoinApi) { - 1 * getTx(txid) >> Mono.just([ - txid: txid - ]) - } + Head head = Mock(Head) { _ * getFlux() >> Flux.empty() } @@ -228,17 +226,20 @@ class TrackBitcoinTxSpec extends Specification { Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over ] } - BitcoinUpstream upstream = Mock(BitcoinUpstream) { - _ * getApi(_) >> Mono.just(api) + BitcoinReader api = Mock(BitcoinReader) { + 1 * getTx(txid) >> Mono.just([ + txid: txid + ]) + _ * getMempool() >> mempoolAccess + } + BitcoinMultistream upstream = Mock(BitcoinMultistream) { _ * getHead() >> head - _ * getData() >> Mock(BitcoinReader) { - _ * getMempool() >> mempoolAccess - } + _ * getReader() >> api } when: def steps = StepVerifier.withVirtualTime { - track.untilFound(Chain.BITCOIN, api, upstream, txid).take(1) + track.untilFound(Chain.BITCOIN, upstream, txid).take(1) } then: @@ -252,9 +253,9 @@ class TrackBitcoinTxSpec extends Specification { def "Subscribe to an existing tx"() { setup: - TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - DirectBitcoinApi api = Mock(DirectBitcoinApi) { + BitcoinReader api = Mock(BitcoinReader) { _ * getTx(txid) >> Mono.just([ txid : txid, blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", @@ -267,18 +268,18 @@ class TrackBitcoinTxSpec extends Specification { ]) } def next = Flux.fromIterable([10, 11, 12]).map { h -> - new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, []) + new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, []) } Head head = Mock(Head) { _ * getFlux() >> next } - BitcoinUpstream upstream = Mock(BitcoinUpstream) { - _ * getApi(_) >> Mono.just(api) + BitcoinMultistream upstream = Mock(BitcoinMultistream) { + _ * getReader() >> api _ * getHead() >> head } when: - def act = track.subscribe(Chain.BITCOIN, api, upstream, txid) + def act = track.subscribe(Chain.BITCOIN, upstream, txid) then: StepVerifier.create(act) diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy index 53408786..654a7133 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy @@ -19,21 +19,12 @@ package io.emeraldpay.dshackle.rpc import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.data.BlockContainer -import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.test.TestingCommons -import io.emeraldpay.dshackle.test.UpstreamsMock -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader +import io.emeraldpay.dshackle.test.MultistreamHolderMock +import io.emeraldpay.dshackle.upstream.MultistreamHolder import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.BlockHash -import io.infinitape.etherjar.rpc.ReactorRpcClient -import io.infinitape.etherjar.rpc.RpcCall import io.infinitape.etherjar.rpc.json.BlockJson -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.core.publisher.TopicProcessor -import reactor.core.scheduler.Schedulers import reactor.test.StepVerifier import spock.lang.Specification @@ -64,9 +55,9 @@ class TrackEthereumAddressSpec extends Specification { .setBalance("1234567890") .build() - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams) apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") @@ -104,9 +95,9 @@ class TrackEthereumAddressSpec extends Specification { return it } - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams) apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") @@ -121,6 +112,6 @@ class TrackEthereumAddressSpec extends Specification { } .expectNext(exp2).as("Second block") .thenCancel() - .verify(Duration.ofSeconds(1)) + .verify(Duration.ofSeconds(2)) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy index aa0b7fb0..72d4a60c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy @@ -23,15 +23,13 @@ import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.test.TestingCommons -import io.emeraldpay.dshackle.test.UpstreamsMock +import io.emeraldpay.dshackle.test.MultistreamHolderMock import io.emeraldpay.dshackle.upstream.Head -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.TransactionId -import io.infinitape.etherjar.rpc.ReactorRpcClient import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionRefJson @@ -98,9 +96,9 @@ class TrackEthereumTxSpec extends Specification { .setTimestamp(blockJson.timestamp.toEpochMilli()) ).build() - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) apiMock.answer("eth_getTransactionByHash", [txId], txJson) @@ -118,10 +116,10 @@ class TrackEthereumTxSpec extends Specification { def "Wait for unknown transaction"() { setup: - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) - ((AggregatedEthereumUpstreams) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) { + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) + ((EthereumMultistream) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) { _ * getFlux() >> Flux.empty() } TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) @@ -133,7 +131,7 @@ class TrackEthereumTxSpec extends Specification { when: def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6) def act = StepVerifier.withVirtualTime( - { trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).castApi(EthereumApi.class)) }, + { trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).cast(EthereumMultistream)) }, { scheduler }, 5) @@ -168,9 +166,9 @@ class TrackEthereumTxSpec extends Specification { it } - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) def scheduler = VirtualTimeScheduler.create(true) trackTx.scheduler = scheduler @@ -193,14 +191,14 @@ class TrackEthereumTxSpec extends Specification { def "New block makes tx mined"() { setup: - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6) def block = new BlockContainer( - 100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, + 100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null, [TxId.from(txId)] ) @@ -215,14 +213,14 @@ class TrackEthereumTxSpec extends Specification { def "New block without current tx requires a call"() { setup: - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6) def block = new BlockContainer( - 100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, + 100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null, [TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")] ) apiMock.answer("eth_getTransactionByHash", [txId], null) @@ -288,9 +286,9 @@ class TrackEthereumTxSpec extends Specification { ) - def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) + def apiMock = TestingCommons.api() def upstreamMock = TestingCommons.upstream(apiMock) - Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) + MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) apiMock.answerOnce("eth_getTransactionByHash", [txId], null) diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy index 6701f1e7..1abad589 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy @@ -19,11 +19,10 @@ package io.emeraldpay.dshackle.test import com.fasterxml.jackson.databind.ObjectMapper import com.google.protobuf.ByteString import io.emeraldpay.api.proto.BlockchainOuterClass -import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.emeraldpay.grpc.Chain +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.grpc.stub.StreamObserver -import io.infinitape.etherjar.rpc.ReactorRpcClient import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.json.ResponseJson import org.jetbrains.annotations.NotNull @@ -31,16 +30,18 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import reactor.core.publisher.Mono +import java.time.Duration import java.util.concurrent.Callable -class EthereumApiMock extends DirectEthereumApi { +class EthereumApiMock implements Reader { private static final Logger log = LoggerFactory.getLogger(this) List predefined = [] private ObjectMapper objectMapper - EthereumApiMock(@NotNull ReactorRpcClient rpcClient, @NotNull ObjectMapper objectMapper, @NotNull Chain chain) { - super(rpcClient, null, objectMapper, new DirectCallMethods()) + String id = "default" + + EthereumApiMock(@NotNull ObjectMapper objectMapper) { this.objectMapper = objectMapper } @@ -55,10 +56,11 @@ class EthereumApiMock extends DirectEthereumApi { } @Override - Mono execute(int id, @NotNull String method, @NotNull List params) { - Callable call = { - def predefined = predefined.find { it.isSame(id, method, params) } - ResponseJson json = new ResponseJson(id: id) + Mono read(JsonRpcRequest request) { + Callable call = { + def predefined = predefined.find { it.isSame(request.method, request.params) } + byte[] result = null + JsonRpcResponse.ResponseError error = null if (predefined != null) { if (predefined.exception != null) { predefined.onCalled() @@ -66,32 +68,37 @@ class EthereumApiMock extends DirectEthereumApi { throw predefined.exception } if (predefined.result instanceof RpcResponseError) { - json.error = predefined.result + ((RpcResponseError) predefined.result).with { err -> + error = new JsonRpcResponse.ResponseError(err.code, err.message) + } } else { - json.result = predefined.result +// ResponseJson json = new ResponseJson(id: 1, result: predefined.result) + result = objectMapper.writeValueAsBytes(predefined.result) } predefined.onCalled() predefined.print() } else { - log.error("Method ${method} with ${params} is not mocked") - json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked") + log.error("Method ${request.method} with ${request.params} is not mocked") + error = new JsonRpcResponse.ResponseError(-32601, "Method ${request.method} with ${request.params} is not mocked") } - byte[] result = objectMapper.writeValueAsBytes(json) - return result - } as Callable + return new JsonRpcResponse(result, error) + } as Callable return Mono.fromCallable(call) } def nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver responseObserver) { request.itemsList.forEach { req -> - def resp = execute(req.id, req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray())) - resp.subscribe { - def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder() - .setId(req.id) - .setSucceed(true) - .setPayload(ByteString.copyFrom(resp.block())) - responseObserver.onNext(proto.build()) + JsonRpcResponse resp = read(new JsonRpcRequest(req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray()))) + .block(Duration.ofSeconds(5)) + def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder() + .setId(req.id) + .setSucceed(resp.hasResult()) + .setPayload(ByteString.copyFrom(resp.getResult())) + + resp.error?.with { err -> + proto.setErrorMessage(err.message) } + responseObserver.onNext(proto.build()) } responseObserver.onCompleted() } @@ -103,7 +110,7 @@ class EthereumApiMock extends DirectEthereumApi { Integer limit Throwable exception - boolean isSame(int id, String method, List params) { + boolean isSame(String method, List params) { if (limit != null) { if (limit <= 0) { return false diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiStub.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiStub.groovy index 8601e310..ea6b9b0a 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiStub.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiStub.groovy @@ -16,28 +16,20 @@ */ package io.emeraldpay.dshackle.test -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.infinitape.etherjar.rpc.ReactorBatch -import io.infinitape.etherjar.rpc.ReactorRpcClient -import io.infinitape.etherjar.rpc.RpcCall -import io.infinitape.etherjar.rpc.RpcCallResponse -import reactor.core.publisher.Flux +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import reactor.core.publisher.Mono -class EthereumApiStub extends DirectEthereumApi { +class EthereumApiStub implements Reader { private String id - private static ObjectMapper objectMapper = TestingCommons.objectMapper() - private static ReactorRpcClient rpcClient = new RpcClientMock(); EthereumApiStub(Integer id) { this(id.toString()) } EthereumApiStub(String id) { - super(rpcClient, null, objectMapper, new DirectCallMethods()) this.id = id } @@ -46,16 +38,9 @@ class EthereumApiStub extends DirectEthereumApi { return "API Stub $id" } - static class RpcClientMock implements ReactorRpcClient { - - @Override - Flux execute(ReactorBatch batch) { - return Flux.error(new Exception("Not implemented in mock")) - } - - @Override - def Mono execute(RpcCall call) { - return Mono.error(new Exception("Not implemented in mock")) - } + @Override + Mono read(JsonRpcRequest key) { + return Mono.error(new Exception("Not implemented in mock")) } + } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy index 46810e8d..36dd9d33 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy @@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.test import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.upstream.Head +import org.jetbrains.annotations.NotNull import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.core.publisher.Mono @@ -28,9 +29,14 @@ class EthereumHeadMock implements Head { private TopicProcessor bus = TopicProcessor.create() private Publisher predefined = null private BlockContainer latest + private List handlers = [] void nextBlock(BlockContainer block) { + handlers.forEach { + it.run() + } assert block != null + println("New block: ${block.height} / ${block.hash}") latest = block bus.onNext(block) } @@ -51,4 +57,9 @@ class EthereumHeadMock implements Head { return Flux.concat(Mono.justOrEmpty(latest), bus).distinctUntilChanged() } } + + @Override + void onBeforeBlock(@NotNull Runnable handler) { + handlers.add(handler) + } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy index 74ff3398..9bfdbef0 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy @@ -16,18 +16,22 @@ */ package io.emeraldpay.dshackle.test + import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.startup.QuorumForLabels +import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi +import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.domain.TransactionId -import io.infinitape.etherjar.rpc.json.BlockJson import org.jetbrains.annotations.NotNull import org.reactivestreams.Publisher @@ -35,24 +39,33 @@ class EthereumUpstreamMock extends EthereumUpstream { EthereumHeadMock ethereumHeadMock = new EthereumHeadMock() - EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) { - this(chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain)) + static CallMethods allMethods() { + new AggregatedCallMethods([ + new DefaultEthereumMethods(TestingCommons.objectMapper(), Chain.ETHEREUM), + new DefaultBitcoinMethods(TestingCommons.objectMapper()), + new DirectCallMethods(["eth_test"]) + ]) } - EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) { - this(id, chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain)) + EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader api) { + this(chain, api, allMethods()) } - EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) { + EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader api) { + this(id, chain, api, allMethods()) + } + + EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader api, CallMethods methods) { this("test", chain, api, methods) } - EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) { + EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader api, CallMethods methods) { super(id, chain, api, null, UpstreamsConfig.Options.getDefaults(), new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()), methods, TestingCommons.objectMapper()) setLag(0) setStatus(UpstreamAvailability.OK) + start() } void nextBlock(BlockContainer block) { diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/UpstreamsMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/MultistreamHolderMock.groovy similarity index 51% rename from src/test/groovy/io/emeraldpay/dshackle/test/UpstreamsMock.groovy rename to src/test/groovy/io/emeraldpay/dshackle/test/MultistreamHolderMock.groovy index 4f1f6ecb..6c8de274 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/UpstreamsMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/MultistreamHolderMock.groovy @@ -16,48 +16,60 @@ */ package io.emeraldpay.dshackle.test -import com.fasterxml.jackson.databind.ObjectMapper + +import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.upstream.AggregatedUpstream +import io.emeraldpay.dshackle.upstream.Multistream +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.Upstream -import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams +import io.emeraldpay.dshackle.upstream.MultistreamHolder +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.grpc.Chain import org.jetbrains.annotations.NotNull import reactor.core.publisher.Flux -class UpstreamsMock implements Upstreams { +class MultistreamHolderMock implements MultistreamHolder { private Map target = [:] - private Map upstreams = [:] + private Map upstreams = [:] - UpstreamsMock(Chain chain, Upstream up) { + MultistreamHolderMock(Chain chain, Upstream up) { addUpstream(chain, up) } - UpstreamsMock(Chain chain1, Upstream up1, Chain chain2, Upstream up2) { - addUpstream(chain1, up1) - addUpstream(chain2, up2) - } - AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull EthereumUpstream up) { + Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) { if (!upstreams.containsKey(chain)) { - upstreams[chain] = new AggregatedEthereumUpstreamsMock(chain, [up], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper()) - upstreams[chain].start() + if (BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM) { + if (up instanceof EthereumMultistream) { + upstreams[chain] = up + } else if (up instanceof EthereumUpstream) { + upstreams[chain] = new EthereumMultistreamMock(chain, [up as EthereumUpstream], Caches.default(TestingCommons.objectMapper())) + } else { + throw new IllegalArgumentException("Unsupported upstream type ${up.class}") + } + upstreams[chain].start() + } else if (BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN) { + if (up instanceof BitcoinMultistream) { + upstreams[chain] = up + } else if (up instanceof BitcoinUpstream) { + upstreams[chain] = new BitcoinMultistream(chain, [up as BitcoinUpstream], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper()) + } else { + throw new IllegalArgumentException("Unsupported upstream type ${up.class}") + } + upstreams[chain].start() + } } else { upstreams[chain].addUpstream(up) } return upstreams[chain] } - void setReader(@NotNull Chain chain, EthereumReader reader) { - upstreams[chain].customReader = reader - } - @Override - AggregatedUpstream getUpstream(@NotNull Chain chain) { + Multistream getUpstream(@NotNull Chain chain) { return upstreams[chain] } @@ -85,12 +97,12 @@ class UpstreamsMock implements Upstreams { return upstreams.containsKey(chain) } - static class AggregatedEthereumUpstreamsMock extends AggregatedEthereumUpstreams { + static class EthereumMultistreamMock extends EthereumMultistream { EthereumReader customReader = null - AggregatedEthereumUpstreamsMock(@NotNull Chain chain, @NotNull List upstreams, @NotNull Caches caches, @NotNull ObjectMapper objectMapper) { - super(chain, upstreams, caches, objectMapper) + EthereumMultistreamMock(@NotNull Chain chain, @NotNull List upstreams, @NotNull Caches caches) { + super(chain, upstreams, caches, TestingCommons.objectMapper()) } @Override diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamApi.kt b/src/test/groovy/io/emeraldpay/dshackle/test/ReaderMock.groovy similarity index 58% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamApi.kt rename to src/test/groovy/io/emeraldpay/dshackle/test/ReaderMock.groovy index 14c11104..9cb51595 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamApi.kt +++ b/src/test/groovy/io/emeraldpay/dshackle/test/ReaderMock.groovy @@ -1,6 +1,5 @@ /** * Copyright (c) 2020 EmeraldPay, Inc - * Copyright (c) 2020 ETCDEV GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,20 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream +package io.emeraldpay.dshackle.test +import io.emeraldpay.dshackle.reader.Reader import reactor.core.publisher.Mono -/** - * A general interface to make a request to an Upstream API - */ -interface UpstreamApi { +class ReaderMock implements Reader { - /** - * @param id an internal uniq id, if multiple requests are made in batch - * @param method JSON RPC method name - * @param params JSON RPC parameters, must be serializable into a JSON array - */ - fun execute(id: Int, method: String, params: List): Mono + private Map mapping = new HashMap() -} \ No newline at end of file + ReaderMock() { + } + + ReaderMock with(K key, D data) { + mapping[key] = data + return this + } + + @Override + Mono read(K key) { + return Mono.justOrEmpty(mapping.get(key)) + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy index 867f63bc..df2fb400 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/TestingCommons.groovy @@ -24,14 +24,15 @@ 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.upstream.AggregatedUpstream +import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream 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.rpc.JacksonRpcConverter -import io.infinitape.etherjar.rpc.ReactorRpcClient import java.text.SimpleDateFormat @@ -50,32 +51,34 @@ class TestingCommons { return objectMapper } - static EthereumApiMock api(ReactorRpcClient rpcClient) { - return new EthereumApiMock(rpcClient, objectMapper(), Chain.ETHEREUM) + static EthereumApiMock api() { + return new EthereumApiMock(objectMapper()) } static JacksonRpcConverter rpcConverter() { return new JacksonRpcConverter(objectMapper()) } - static EthereumUpstreamMock upstream(DirectEthereumApi api) { + static EthereumUpstreamMock upstream(Reader api) { return new EthereumUpstreamMock(Chain.ETHEREUM, api) } - static EthereumUpstreamMock upstream(DirectEthereumApi api, String method) { + static EthereumUpstreamMock upstream(Reader api, String method) { return upstream(api, [method]) } - static EthereumUpstreamMock upstream(DirectEthereumApi api, List methods) { + static EthereumUpstreamMock upstream(Reader api, List methods) { return new EthereumUpstreamMock(Chain.ETHEREUM, api, new DirectCallMethods(methods)) } - static AggregatedUpstream aggregatedUpstream(DirectEthereumApi api) { + static Multistream aggregatedUpstream(Reader api) { return aggregatedUpstream(upstream(api)) } - static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) { - return new AggregatedEthereumUpstreams(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper()) + static Multistream aggregatedUpstream(EthereumUpstream up) { + return new EthereumMultistream(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper()).tap { + start() + } } static CachesFactory emptyCaches() { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/AbstractHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/AbstractHeadSpec.groovy new file mode 100644 index 00000000..a0a022f8 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/AbstractHeadSpec.groovy @@ -0,0 +1,117 @@ +/** + * 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 + +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import reactor.core.publisher.Flux +import reactor.core.publisher.TopicProcessor +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration +import java.time.Instant + +class AbstractHeadSpec extends Specification { + + def blocks = [1L, 2, 3, 4].collect { i -> + byte[] hash = new byte[32] + hash[0] = i as byte + new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, []) + } + + def "Calls beforeBlock on each block"() { + setup: + TopicProcessor source = TopicProcessor.create() + def head = new TestHead() + def called = false + when: + head.follow(Flux.from(source)) + head.onBeforeBlock { + called = true + } + def act = head.flux + source.onNext(blocks[0]) + then: + StepVerifier.create(act) + .expectNext(blocks[0]) + .then { + assert called + called = false + source.onNext(blocks[1]) + } + .expectNext(blocks[1]) + .then { + assert called + source.onComplete() + } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "Follows source"() { + setup: + TopicProcessor source = TopicProcessor.create() + def head = new TestHead() + when: + head.follow(Flux.from(source)) + def act = head.flux + source.onNext(blocks[0]) + then: + StepVerifier.create(act) + .expectNext(blocks[0]) + .then { source.onNext(blocks[1]) } + .expectNext(blocks[1]) + .then { source.onNext(blocks[2]) } + .expectNext(blocks[2]) + .then { source.onNext(blocks[3]) } + .expectNext(blocks[3]) + .then { source.onComplete() } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "Ignores block will less difficulty"() { + setup: + TopicProcessor source = TopicProcessor.create() + def head = new TestHead() + def wrongblock = new BlockContainer( + blocks[1].height, BlockId.from(blocks[1].hash.value.clone().tap { it[1] = 0xff as byte }), + blocks[1].difficulty - 1, + Instant.now(), + false, null, null, [] + ) + when: + head.follow(Flux.from(source)) + def act = head.flux + source.onNext(blocks[0]) + then: + StepVerifier.create(act) + .expectNext(blocks[0]) + .then { source.onNext(blocks[1]) } + .expectNext(blocks[1]) + .then { source.onNext(wrongblock) } + .then { source.onNext(blocks[3]) } + .expectNext(blocks[3]) + .then { source.onComplete() } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + class TestHead extends AbstractHead { + + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy deleted file mode 100644 index f7c7fde3..00000000 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy +++ /dev/null @@ -1,261 +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 - -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.dshackle.cache.BlockByHeight -import io.emeraldpay.dshackle.cache.BlocksMemCache -import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.cache.HeightCache -import io.emeraldpay.dshackle.cache.TxMemCache -import io.emeraldpay.dshackle.data.BlockContainer -import io.emeraldpay.dshackle.data.BlockId -import io.emeraldpay.dshackle.data.TxId -import io.emeraldpay.dshackle.test.TestingCommons -import io.infinitape.etherjar.domain.BlockHash -import io.infinitape.etherjar.domain.TransactionId -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.test.StepVerifier -import spock.lang.Specification - -import java.time.Duration -import java.time.Instant -import java.time.temporal.ChronoUnit - -class CachingEthereumApiSpec extends Specification { - - ObjectMapper objectMapper = TestingCommons.objectMapper() - - def "Get blockNumber from head"() { - setup: - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.default(objectMapper), - head - ) - 1 * head.getFlux() >> Flux.just(BlockContainer.from( - new BlockJson( - number: 100, - hash: BlockHash.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58"), - difficulty: 1, - totalDifficulty: BigInteger.ONE, - timestamp: Instant.now().truncatedTo(ChronoUnit.SECONDS) - ), - objectMapper - )) - when: - def act = api.execute(1, "eth_blockNumber", []).map { new String(it) } - - then: - StepVerifier.create(act) - .expectNext('{"jsonrpc":"2.0","id":1,"result":"0x64"}') - .expectComplete() - .verify(Duration.ofSeconds(3)) - } - - def "Return empty if block is not cached"() { - setup: - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.default(objectMapper), - head - ) - when: - def act = api.execute(1, "eth_getBlockByHash", ["0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58", false]).map { new String(it)} - - then: - StepVerifier.create(act) - .expectComplete() - .verify(Duration.ofSeconds(3)) - } - - def "Return block by hash when cached"() { - setup: - def cache = new BlocksMemCache(); - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(cache).build(), - head - ) - cache.add(BlockContainer.from( - new BlockJson( - number: 100, - hash: BlockHash.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58"), - totalDifficulty: BigInteger.ONE, - timestamp: Instant.ofEpochSecond(0x5e95313a) - ), - objectMapper - )) - - when: - def act = api.execute(1, "eth_getBlockByHash", ["0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58", false]).map { new String(it) } - - then: - StepVerifier.create(act) - .expectNext('{"jsonrpc":"2.0","id":1,"result":{"number":"0x64","hash":"0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58","timestamp":"0x5e95313a","transactions":[],"totalDifficulty":"0x1","uncles":[]}}') - .expectComplete() - .verify(Duration.ofSeconds(3)) - } - - def "Return block by height when cached"() { - setup: - def blocksCache = new BlocksMemCache() - def heightCache = new HeightCache() - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setBlockByHeight(heightCache).build(), - head - ) - def block = new BlockJson( - number: 100, - hash: BlockHash.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58"), - totalDifficulty: BigInteger.ONE, - timestamp: Instant.ofEpochSecond(0x5e95313a) - ) - heightCache.add(BlockContainer.from(block, objectMapper)) - blocksCache.add(BlockContainer.from(block, objectMapper)) - - when: - def act = api.execute(1, "eth_getBlockByNumber", ["0x64", false]).map { new String(it) } - - then: - StepVerifier.create(act) - .expectNext('{"jsonrpc":"2.0","id":1,"result":{"number":"0x64","hash":"0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58","timestamp":"0x5e95313a","transactions":[],"totalDifficulty":"0x1","uncles":[]}}') - .expectComplete() - .verify(Duration.ofSeconds(3)) - } - - def "Uses base cache when requested, by hash"() { - setup: - def blocksCache = Mock(BlocksMemCache) - def txCache = Mock(TxMemCache) - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).build(), - head - ) - def block = new BlockJson( - number: 100, - hash: BlockHash.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58"), - totalDifficulty: BigInteger.ONE, - timestamp: Instant.now().truncatedTo(ChronoUnit.SECONDS) - ) - - when: - def act = api.readBlockByHash(1, "eth_getBlockByHash", ["0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58", false]).block() - - then: - act != null - 1 * blocksCache.read(BlockId.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58")) >> Mono.just(BlockContainer.from(block, objectMapper)) - 0 * txCache.read(_) - } - - def "Uses full cache when requested, by hash"() { - setup: - def blocksCache = Mock(BlocksMemCache) - def txCache = Mock(TxMemCache) - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).build(), - head - ) - def block = new BlockJson( - number: 100, - hash: BlockHash.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58"), - totalDifficulty: BigInteger.ONE, - timestamp: Instant.now().truncatedTo(ChronoUnit.SECONDS) - ) - block.transactions = [ - new TransactionRefJson(TransactionId.from("0x0500219f2b147f3013e9030d585e8e5d45401ebd2620a42c879c0d5d1b754073")) - ] - - when: - def act = api.readBlockByHash(1, "eth_getBlockByHash", ["0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58", true]).block() - - then: - act == null - 1 * blocksCache.read(BlockId.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58")) >> Mono.just(BlockContainer.from(block, objectMapper)) - 1 * txCache.read(TxId.from("0x0500219f2b147f3013e9030d585e8e5d45401ebd2620a42c879c0d5d1b754073")) >> Mono.empty() - } - - def "Uses base cache when requested, by height"() { - setup: - def blocksCache = Mock(BlocksMemCache) - def txCache = Mock(TxMemCache) - def heightCache = Mock(HeightCache) - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).setBlockByHeight(heightCache).build(), - head - ) - def block = new BlockJson( - number: 100, - hash: BlockHash.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58"), - totalDifficulty: BigInteger.ONE, - timestamp: Instant.now().truncatedTo(ChronoUnit.SECONDS) - ) - - when: - def act = api.readBlockByNumber(1, "eth_getBlockByNumber", ["0x64", false]).block() - - then: - act != null - 1 * heightCache.read(100) >> Mono.just(BlockId.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58")) - 1 * blocksCache.read(BlockId.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58")) >> Mono.just(BlockContainer.from(block, objectMapper)) - 0 * txCache.read(_) - } - - def "Uses full cache when requested, by height"() { - setup: - def blocksCache = Mock(BlocksMemCache) - def txCache = Mock(TxMemCache) - def heightCache = Mock(HeightCache) - def head = Mock(Head.class) - def api = new CachingEthereumApi( - objectMapper, - Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).setBlockByHeight(heightCache).build(), - head - ) - def block = new BlockJson( - number: 100, - hash: BlockHash.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58"), - totalDifficulty: BigInteger.ONE, - timestamp: Instant.now().truncatedTo(ChronoUnit.SECONDS) - ) - block.transactions = [ - new TransactionRefJson(TransactionId.from("0x0500219f2b147f3013e9030d585e8e5d45401ebd2620a42c879c0d5d1b754073")) - ] - - when: - def act = api.readBlockByNumber(1, "eth_getBlockByNumber", ["0x64", true]).block() - - then: - act == null - 1 * heightCache.read(100) >> Mono.just(BlockId.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58")) - 1 * blocksCache.read(BlockId.from("0x5b4590a9905fa1c9cc273f32e6dc63b4c512f0ee14edc6fa41c26b416a7b5d58")) >> Mono.just(BlockContainer.from(block, objectMapper)) - 1 * txCache.read(TxId.from("0x0500219f2b147f3013e9030d585e8e5d45401ebd2620a42c879c0d5d1b754073")) >> Mono.empty() - } -} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/CurrentUpstreamsSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/CurrentMultistreamHolderSpec.groovy similarity index 78% rename from src/test/groovy/io/emeraldpay/dshackle/upstream/CurrentUpstreamsSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/CurrentMultistreamHolderSpec.groovy index 4449e451..3d320d7b 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/CurrentUpstreamsSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/CurrentMultistreamHolderSpec.groovy @@ -19,15 +19,14 @@ import io.emeraldpay.dshackle.startup.UpstreamChange import io.emeraldpay.dshackle.test.EthereumUpstreamMock import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.ReactorRpcClient import spock.lang.Specification -class CurrentUpstreamsSpec extends Specification { +class CurrentMultistreamHolderSpec extends Specification { def "add upstream"() { setup: - def current = new CurrentUpstreams(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) - def up = new EthereumUpstreamMock("test", Chain.ETHEREUM, TestingCommons.api(Stub(ReactorRpcClient))) + def current = new CurrentMultistreamHolder(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) + def up = new EthereumUpstreamMock("test", Chain.ETHEREUM, TestingCommons.api()) when: current.update(new UpstreamChange(Chain.ETHEREUM, up, UpstreamChange.ChangeType.ADDED)) then: @@ -37,10 +36,10 @@ class CurrentUpstreamsSpec extends Specification { def "add multiple upstreams"() { setup: - def current = new CurrentUpstreams(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) - def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api(Stub(ReactorRpcClient))) - def up2 = new EthereumUpstreamMock("test2", Chain.ETHEREUM_CLASSIC, TestingCommons.api(Stub(ReactorRpcClient))) - def up3 = new EthereumUpstreamMock("test3", Chain.ETHEREUM, TestingCommons.api(Stub(ReactorRpcClient))) + def current = new CurrentMultistreamHolder(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) + def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api()) + def up2 = new EthereumUpstreamMock("test2", Chain.ETHEREUM_CLASSIC, TestingCommons.api()) + def up3 = new EthereumUpstreamMock("test3", Chain.ETHEREUM, TestingCommons.api()) when: current.update(new UpstreamChange(Chain.ETHEREUM, up1, UpstreamChange.ChangeType.ADDED)) current.update(new UpstreamChange(Chain.ETHEREUM_CLASSIC, up2, UpstreamChange.ChangeType.ADDED)) @@ -53,11 +52,11 @@ class CurrentUpstreamsSpec extends Specification { def "remove upstream"() { setup: - def current = new CurrentUpstreams(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) - def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api(Stub(ReactorRpcClient))) - def up2 = new EthereumUpstreamMock("test2", Chain.ETHEREUM_CLASSIC, TestingCommons.api(Stub(ReactorRpcClient))) - def up3 = new EthereumUpstreamMock("test3", Chain.ETHEREUM, TestingCommons.api(Stub(ReactorRpcClient))) - def up1_del = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api(Stub(ReactorRpcClient))) + def current = new CurrentMultistreamHolder(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) + def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api()) + def up2 = new EthereumUpstreamMock("test2", Chain.ETHEREUM_CLASSIC, TestingCommons.api()) + def up3 = new EthereumUpstreamMock("test3", Chain.ETHEREUM, TestingCommons.api()) + def up1_del = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api()) when: current.update(new UpstreamChange(Chain.ETHEREUM, up1, UpstreamChange.ChangeType.ADDED)) current.update(new UpstreamChange(Chain.ETHEREUM_CLASSIC, up2, UpstreamChange.ChangeType.ADDED)) @@ -71,8 +70,8 @@ class CurrentUpstreamsSpec extends Specification { def "available after adding"() { setup: - def current = new CurrentUpstreams(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) - def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api(Stub(ReactorRpcClient))) + def current = new CurrentMultistreamHolder(TestingCommons.objectMapper(), TestingCommons.emptyCaches()) + def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api()) when: def act = current.isAvailable(Chain.ETHEREUM) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy index de841799..1eba282f 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/FilteredApisSpec.groovy @@ -16,14 +16,14 @@ */ package io.emeraldpay.dshackle.upstream +import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.test.EthereumApiStub import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream -import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs +import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.rpc.ReactorRpcClient import reactor.test.StepVerifier @@ -40,6 +40,7 @@ class FilteredApisSpec extends Specification { def "Verifies labels"() { setup: + def i = 0 List upstreams = [ [test: "foo"], [test: "bar"], @@ -50,8 +51,8 @@ class FilteredApisSpec extends Specification { new EthereumUpstream( "test", Chain.ETHEREUM, - new DirectEthereumApi(rpcClient, null, objectMapper, ethereumTargets), - (EthereumWs) null, + TestingCommons.api().tap { it.id = "${i++}" }, + (EthereumWsFactory) null, new UpstreamsConfig.Options(), new QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(it)), ethereumTargets, TestingCommons.objectMapper() @@ -67,9 +68,9 @@ class FilteredApisSpec extends Specification { iter.request(10) then: StepVerifier.create(iter) - .expectNext(upstreams[0].api) - .expectNext(upstreams[2].api) - .expectNext(upstreams[3].api) + .expectNext(upstreams[0]) + .expectNext(upstreams[2]) + .expectNext(upstreams[3]) .expectComplete() .verify(Duration.ofSeconds(1)) @@ -78,9 +79,9 @@ class FilteredApisSpec extends Specification { iter.request(10) then: StepVerifier.create(iter) - .expectNext(upstreams[2].api) - .expectNext(upstreams[3].api) - .expectNext(upstreams[0].api) + .expectNext(upstreams[2]) + .expectNext(upstreams[3]) + .expectNext(upstreams[0]) .expectComplete() .verify(Duration.ofSeconds(1)) @@ -89,12 +90,12 @@ class FilteredApisSpec extends Specification { iter.request(10) then: StepVerifier.create(iter) - .expectNext(upstreams[2].api) - .expectNext(upstreams[3].api) - .expectNext(upstreams[0].api) - .expectNext(upstreams[2].api) - .expectNext(upstreams[3].api) - .expectNext(upstreams[0].api) + .expectNext(upstreams[2]) + .expectNext(upstreams[3]) + .expectNext(upstreams[0]) + .expectNext(upstreams[2]) + .expectNext(upstreams[3]) + .expectNext(upstreams[0]) .expectComplete() .verify(Duration.ofSeconds(1)) } @@ -143,8 +144,8 @@ class FilteredApisSpec extends Specification { def "Makes pause between batches"() { when: - def api1 = TestingCommons.api(Stub(ReactorRpcClient)) - def api2 = TestingCommons.api(Stub(ReactorRpcClient)) + def api1 = TestingCommons.api() + def api2 = TestingCommons.api() def up1 = TestingCommons.upstream(api1) def up2 = TestingCommons.upstream(api2) then: @@ -153,13 +154,13 @@ class FilteredApisSpec extends Specification { apis.request(10) return apis }) - .expectNext(api1, api2).as("Batch 1") - .expectNoEvent(Duration.ofMillis(100)).as("Wait 1") - .expectNext(api1, api2).as("Batch 2") - .expectNoEvent(Duration.ofMillis(400)).as("Wait 2") - .expectNext(api1, api2).as("Batch 3") - .expectNoEvent(Duration.ofMillis(900)).as("Wait 3") - .expectNext(api1, api2).as("Batch 4") + .expectNext(up1, up2).as("Batch 1") + .expectNoEvent(Duration.ofMillis(100)).as("Wait 1") + .expectNext(up1, up2).as("Batch 2") + .expectNoEvent(Duration.ofMillis(400)).as("Wait 2") + .expectNext(up1, up2).as("Batch 3") + .expectNoEvent(Duration.ofMillis(900)).as("Wait 3") + .expectNext(up1, up2).as("Batch 4") .expectComplete() .verify(Duration.ofSeconds(10)) } @@ -177,7 +178,7 @@ class FilteredApisSpec extends Specification { act.request(10) then: StepVerifier.create(act) - .expectNext(apis[2], apis[3], apis[4], apis[5], apis[0], apis[1]) + .expectNext(ups[2], ups[3], ups[4], ups[5], ups[0], ups[1]) .expectComplete() .verify(Duration.ofSeconds(1)) } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/AggregatedUpstreamSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/MultistreamSpec.groovy similarity index 75% rename from src/test/groovy/io/emeraldpay/dshackle/upstream/AggregatedUpstreamSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/MultistreamSpec.groovy index 0414a71c..aa243140 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/AggregatedUpstreamSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/MultistreamSpec.groovy @@ -21,18 +21,17 @@ import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.test.EthereumUpstreamMock import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods -import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams +import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream import io.emeraldpay.grpc.Chain import spock.lang.Specification -class AggregatedUpstreamSpec extends Specification { +class MultistreamSpec extends Specification { def "Aggregates methods"() { setup: - def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test1", "eth_test2"])) - def up2 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test2", "eth_test3"])) - def aggr = new AggregatedEthereumUpstreams(Chain.ETHEREUM, [up1, up2], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper()) + def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api(), new DirectCallMethods(["eth_test1", "eth_test2"])) + def up2 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api(), new DirectCallMethods(["eth_test2", "eth_test3"])) + def aggr = new EthereumMultistream(Chain.ETHEREUM, [up1, up2], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper()) when: aggr.onUpstreamsUpdated() def act = aggr.getMethods() diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy index 07cd3215..f33f7876 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy @@ -15,7 +15,10 @@ */ package io.emeraldpay.dshackle.upstream.bitcoin +import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import reactor.core.publisher.Mono import reactor.test.StepVerifier import spock.lang.Specification @@ -26,8 +29,8 @@ class BitcoinRpcHeadSpec extends Specification { def "Follow 2 blocks created over 3 requests"() { setup: - String hash1 = "0000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50001" - String hash2 = "0000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50002" + String hash1 = "1000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50001" + String hash2 = "2000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50002" def block1 = """ { @@ -74,12 +77,14 @@ class BitcoinRpcHeadSpec extends Specification { } """ - DirectBitcoinApi api = Mock(DirectBitcoinApi) { - _ * executeAndResult(_, "getbestblockhash", _, String) >>> [ - Mono.just(hash1), Mono.just(hash1), Mono.just(hash2) + def api = Mock(Reader) { + _ * read(new JsonRpcRequest("getbestblockhash", [])) >>> [ + Mono.just(new JsonRpcResponse("\"$hash1\"".bytes, null)), + Mono.just(new JsonRpcResponse("\"$hash1\"".bytes, null)), + Mono.just(new JsonRpcResponse("\"$hash2\"".bytes, null)) ] - _ * execute(_, "getblock", [hash1]) >> Mono.just(block1.bytes) - _ * execute(_, "getblock", [hash2]) >> Mono.just(block2.bytes) + _ * read(new JsonRpcRequest("getblock", [hash1])) >> Mono.just(new JsonRpcResponse(block1.bytes, null)) + _ * read(new JsonRpcRequest("getblock", [hash2])) >> Mono.just(new JsonRpcResponse(block2.bytes, null)) } BitcoinRpcHead head = new BitcoinRpcHead(api, new ExtractBlock(TestingCommons.objectMapper()), Duration.ofMillis(200)) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApiSpec.groovy deleted file mode 100644 index efaf2d7d..00000000 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApiSpec.groovy +++ /dev/null @@ -1,123 +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.bitcoin - -import io.emeraldpay.dshackle.test.TestingCommons -import io.infinitape.etherjar.rpc.RpcException -import org.mockserver.integration.ClientAndServer -import org.mockserver.model.HttpRequest -import org.mockserver.model.HttpResponse -import reactor.test.StepVerifier -import spock.lang.Specification - -import java.time.Duration - -class DirectBitcoinApiSpec extends Specification { - - ClientAndServer mockServer - DirectBitcoinApi api - - def setup() { - mockServer = ClientAndServer.startClientAndServer(18332); - api = new DirectBitcoinApi( - new BitcoinRpcClient("localhost:18332", null), - TestingCommons.objectMapper(), new DefaultBitcoinMethods(TestingCommons.objectMapper()) - ) - } - - def cleanup() { - mockServer.stop() - } - - def "Request simple"() { - setup: - def resp = '{' + - ' "result": "0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90",' + - ' "error": null,' + - ' "id": 15' + - '}' - mockServer.when( - HttpRequest.request() - ).respond( - HttpResponse.response(resp) - ) - when: - def act = api.executeAndResult(15, "getbestblockhash", [], String) - then: - StepVerifier.create(act) - .expectNext("0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90") - .expectComplete() - .verify(Duration.ofSeconds(1)) - mockServer.verify( - HttpRequest.request() - .withMethod("POST") - .withBody('{"jsonrpc":"2.0","method":"getbestblockhash","params":[],"id":15}') - ) - } - - def "Request with params"() { - setup: - def resp = '{' + - ' "result": "something",' + - ' "id": 1' + - '}' - mockServer.when( - HttpRequest.request() - ).respond( - HttpResponse.response(resp) - ) - when: - def act = api.executeAndResult(1, "getsomething", ["something", false], String) - then: - StepVerifier.create(act) - .expectNext("something") - .expectComplete() - .verify(Duration.ofSeconds(1)) - mockServer.verify( - HttpRequest.request() - .withMethod("POST") - .withBody('{"jsonrpc":"2.0","method":"getsomething","params":["something",false],"id":1}') - ) - } - - def "Returns error"() { - setup: - def resp = '{' + - ' "result": null,' + - ' "error": {' + - ' "code": -32601,' + - ' "message": "Method not found"' + - ' },' + - ' "id": 1' + - '}' - mockServer.when( - HttpRequest.request() - ).respond( - HttpResponse.response(resp) - ) - when: - def act = api.executeAndResult(1, "geterror", [], String) - then: - StepVerifier.create(act) - .expectError(RpcException) - .verify(Duration.ofSeconds(1)) - mockServer.verify( - HttpRequest.request() - .withMethod("POST") - .withBody('{"jsonrpc":"2.0","method":"geterror","params":[],"id":1}') - ) - } -} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethodsSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethodsSpec.groovy index f015d1d7..c11149fc 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethodsSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/calls/AggregatedCallMethodsSpec.groovy @@ -84,16 +84,34 @@ class AggregatedCallMethodsSpec extends Specification { setup: def delegate1 = Mock(CallMethods) { _ * getSupportedMethods() >> ["eth_no_test", "foo_bar"] - 1 * isAllowed("eth_test") >> false - 1 * isAllowed("eth_no_test") >> true - 1 * isHardcoded("eth_no_test") >> false } def delegate2 = Mock(CallMethods) { _ * getSupportedMethods() >> ["eth_test", "foo_bar"] - 1 * isAllowed("eth_test") >> true - 1 * isAllowed("eth_no_test") >> false + 1 * isHardcoded("eth_test") >> true + } + def aggregate = new AggregatedCallMethods([delegate1, delegate2]) + when: + def act = aggregate.isHardcoded("eth_test") + then: + act + when: + act = aggregate.isHardcoded("eth_no_test") + then: + !act + } + + def "Can be hardcoded if not allowed"() { + setup: + def delegate1 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_no_test", "foo_bar"] + _ * isAllowed(_) >> false + 1 * isHardcoded("eth_no_test") >> false + } + def delegate2 = Mock(CallMethods) { + _ * getSupportedMethods() >> ["eth_test", "foo_bar"] + _ * isAllowed(_) >> false 1 * isHardcoded("eth_test") >> true } def aggregate = new AggregatedCallMethods([delegate1, delegate2]) @@ -112,11 +130,10 @@ class AggregatedCallMethodsSpec extends Specification { setup: def delegate1 = Mock(CallMethods) { _ * getSupportedMethods() >> ["eth_no_test", "foo_bar"] - 1 * isAllowed("eth_test") >> false + 1 * isHardcoded("eth_test") >> false } def delegate2 = Mock(CallMethods) { _ * getSupportedMethods() >> ["eth_test", "foo_bar"] - 1 * isAllowed("eth_test") >> true 1 * isHardcoded("eth_test") >> true 1 * executeHardcoded("eth_test") >> "hello" } @@ -124,6 +141,6 @@ class AggregatedCallMethodsSpec extends Specification { when: def act = aggregate.executeHardcoded("eth_test") then: - act == "hello" + new String(act) == "hello" } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApiSpec.groovy deleted file mode 100644 index 2452ba97..00000000 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/DirectEthereumApiSpec.groovy +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Copyright (c) 2019 ETCDEV GmbH - * 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.test.TestingCommons -import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods -import io.infinitape.etherjar.rpc.ReactorRpcClient -import io.infinitape.etherjar.rpc.RpcException -import io.infinitape.etherjar.rpc.RpcResponseError -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionJson -import reactor.core.publisher.Mono -import reactor.test.StepVerifier -import spock.lang.Specification - -import java.time.Duration - -class DirectEthereumApiSpec extends Specification { - - DirectEthereumApi api = new DirectEthereumApi(Stub(ReactorRpcClient), null, TestingCommons.objectMapper(), new DirectCallMethods()) - - def "Process successful result"() { - setup: - def result = Mono.just("hello") - when: - def act = api.processResult(1, "eth_test", result) - .map { new String(it) } - - then: - StepVerifier.create(act) - .expectNext('{"jsonrpc":"2.0","id":1,"result":"hello"}') - .expectComplete() - .verify(Duration.ofSeconds(1)) - - } - - def "Process empty result"() { - setup: - def result = Mono.empty() - when: - def act = api.processResult(1, "eth_test", result) - .map { new String(it) } - - then: - StepVerifier.create(act) - .expectNext('{"jsonrpc":"2.0","id":1,"result":null}') - .expectComplete() - .verify(Duration.ofSeconds(1)) - - } - - def "Process standard RPC error"() { - setup: - def result = Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test Error", Map.of("foo", "bar"))) - when: - def act = api.processResult(1, "eth_test", result) - .map { new String(it) } - - then: - StepVerifier.create(act) - .expectNext('{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Test Error","data":{"foo":"bar"}}}') - .expectComplete() - .verify(Duration.ofSeconds(1)) - - } - - def "Process internal exception"() { - setup: - def result = Mono.error(new InterruptedException("test")) - when: - def act = api.processResult(1, "eth_test", result) - .map { new String(it) } - - then: - StepVerifier.create(act) - .expectNext('{"jsonrpc":"2.0","id":1,"error":{"code":-32020,"message":"Error reading from upstream"}}') - .expectComplete() - .verify(Duration.ofSeconds(1)) - - } - - def "Typed mapping for block request"() { - when: - def act = api.callMapping("eth_getBlockByHash", ["0xacf5611707048efc39cabed483e420672ca1ed070f248ef6202c99994dbc6061", false]) - then: - act.jsonType == BlockJson - act.resultType == BlockJson - } - - def "Typed mapping for block request with txes"() { - when: - def act = api.callMapping("eth_getBlockByHash", ["0xacf5611707048efc39cabed483e420672ca1ed070f248ef6202c99994dbc6061", true]) - then: - act.jsonType == BlockJson - act.resultType == BlockJson - } - - def "Typed mapping for block by height request"() { - when: - def act = api.callMapping("eth_getBlockByNumber", ["0x135", false]) - then: - act.jsonType == BlockJson - act.resultType == BlockJson - } - - def "Typed mapping for block by height request with txes"() { - when: - def act = api.callMapping("eth_getBlockByNumber", ["0xacf5", true]) - then: - act.jsonType == BlockJson - act.resultType == BlockJson - } - - def "Typed mapping for tx request"() { - when: - def act = api.callMapping("eth_getTransactionByHash", ["0xacf5611707048efc39cabed483e420672ca1ed070f248ef6202c99994dbc6061"]) - then: - act.jsonType == TransactionJson - act.resultType == TransactionJson - } - - def "Errors for mapping of invalid tx request"() { - when: - api.callMapping("eth_getTransactionByHash", ["0xacf5611707048efc39cabed483e420672ca1ed070f248ef6"]) - then: - def t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - - when: - api.callMapping("eth_getTransactionByHash", ["0xacf5611707048efc39cabed483e420672ca1ed070f248ef6202c99994dbc6061", true]) - then: - t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - - when: - api.callMapping("eth_getTransactionByHash", []) - then: - t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - } - - def "Errors for mapping of invalid block request"() { - when: - api.callMapping("eth_getBlockByHash", ["0xacf5611707048efc39cabed483e420672ca1ed070f248ef6202c99994dbc6061"]) - then: - def t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - - when: - api.callMapping("eth_getBlockByHash", ["0xacf5611707048efc39cabed48f6202c99994dbc6061", true]) - then: - t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - - when: - api.callMapping("eth_getBlockByHash", []) - then: - t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - } - - def "Errors for mapping of invalid block by number request"() { - when: - api.callMapping("eth_getBlockByNumber", ["0xacf5611707048efc3248ef6202c99994dbc6061"]) - then: - def t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - - when: - api.callMapping("eth_getBlockByNumber", ["0x", true]) - then: - t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - - when: - api.callMapping("eth_getBlockByNumber", ["-0x23", true]) - then: - t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - - when: - api.callMapping("eth_getBlockByNumber", []) - then: - t = thrown(RpcException) - t.code == RpcResponseError.CODE_INVALID_METHOD_PARAMS - } -} diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/EthereumBlocksWithTxCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumFullBlocksReaderSpec.groovy similarity index 93% rename from src/test/groovy/io/emeraldpay/dshackle/cache/EthereumBlocksWithTxCacheSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumFullBlocksReaderSpec.groovy index 86eb16e8..ec504b15 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/EthereumBlocksWithTxCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumFullBlocksReaderSpec.groovy @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.cache +package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.cache.BlocksMemCache +import io.emeraldpay.dshackle.cache.TxMemCache import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.TxContainer @@ -29,7 +31,7 @@ import spock.lang.Specification import java.time.Instant -class EthereumBlocksWithTxCacheSpec extends Specification { +class EthereumFullBlocksReaderSpec extends Specification { // sorted String hash1 = "0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5" @@ -119,7 +121,7 @@ class EthereumBlocksWithTxCacheSpec extends Specification { blocks.add(BlockContainer.from(block2, objectMapper)) blocks.add(BlockContainer.from(block3, objectMapper)) - def full = new EthereumBlocksWithTxCache(objectMapper, blocks, txes) + def full = new EthereumFullBlocksReader(objectMapper, blocks, txes) when: def act = full.read(BlockId.from(block1.hash)).block() @@ -185,7 +187,7 @@ class EthereumBlocksWithTxCacheSpec extends Specification { blocks.add(BlockContainer.from(block2, objectMapper)) blocks.add(BlockContainer.from(block3, objectMapper)) - def full = new EthereumBlocksWithTxCache(objectMapper, blocks, txes) + def full = new EthereumFullBlocksReader(objectMapper, blocks, txes) when: def act = full.read(BlockId.from(block3.hash)).block() @@ -205,7 +207,7 @@ class EthereumBlocksWithTxCacheSpec extends Specification { txes.add(TxContainer.from(tx1, objectMapper)) blocks.add(BlockContainer.from(block1, objectMapper)) //missing tx2 in cache - def full = new EthereumBlocksWithTxCache(objectMapper, blocks, txes) + def full = new EthereumFullBlocksReader(objectMapper, blocks, txes) when: def act = full.read(BlockId.from(block1.hash)).block() @@ -223,7 +225,7 @@ class EthereumBlocksWithTxCacheSpec extends Specification { txes.add(TxContainer.from(tx2, objectMapper)) txes.add(TxContainer.from(tx3, objectMapper)) - def full = new EthereumBlocksWithTxCache(objectMapper, blocks, txes) + def full = new EthereumFullBlocksReader(objectMapper, blocks, txes) when: def act = full.read(BlockId.from(block1.hash)).block() diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumReaderSpec.groovy index 18f5afa4..c8ee364c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumReaderSpec.groovy @@ -21,13 +21,14 @@ 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.test.EthereumUpstreamMock import io.emeraldpay.dshackle.test.TestingCommons -import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.Multistream +import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.domain.Wei -import io.infinitape.etherjar.rpc.ReactorRpcClient import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionRefJson @@ -63,7 +64,7 @@ class EthereumReaderSpec extends Specification { .setBlockByHash(memCache) .setObjectMapper(TestingCommons.objectMapper()) .build() - def reader = new EthereumReader(Stub(Upstream), caches, TestingCommons.objectMapper()) + def reader = new EthereumReader(Stub(Multistream), caches, TestingCommons.objectMapper()) when: def act = reader.blocksById().read(blockId).block() @@ -81,8 +82,7 @@ class EthereumReaderSpec extends Specification { .setBlockByHash(memCache) .setObjectMapper(TestingCommons.objectMapper()) .build() - def rpcClient = Stub(ReactorRpcClient) - def api = TestingCommons.api(rpcClient) + def api = TestingCommons.api() api.answer("eth_getBlockByHash", ["0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2", false], blockJson) def upstream = TestingCommons.aggregatedUpstream(api) @@ -104,8 +104,7 @@ class EthereumReaderSpec extends Specification { .setBlockByHash(memCache) .setObjectMapper(TestingCommons.objectMapper()) .build() - def rpcClient = Stub(ReactorRpcClient) - def api = TestingCommons.api(rpcClient) + def api = TestingCommons.api() api.answer("eth_getBlockByHash", ["0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2", false], blockJson) def upstream = TestingCommons.aggregatedUpstream(api) @@ -127,7 +126,7 @@ class EthereumReaderSpec extends Specification { .setBlockByHash(memCache) .setObjectMapper(TestingCommons.objectMapper()) .build() - def reader = new EthereumReader(Stub(Upstream), caches, TestingCommons.objectMapper()) + def reader = new EthereumReader(Stub(Multistream), caches, TestingCommons.objectMapper()) when: def act = reader.blocksByHash().read(blockJson.hash).block() @@ -145,8 +144,7 @@ class EthereumReaderSpec extends Specification { .setBlockByHash(memCache) .setObjectMapper(TestingCommons.objectMapper()) .build() - def rpcClient = Stub(ReactorRpcClient) - def api = TestingCommons.api(rpcClient) + def api = TestingCommons.api() api.answer("eth_getBlockByHash", ["0xf85b826fdf98ee0f48f7db001be00472e63ceb056846f4ecac5f0c32878b8ab2", false], blockJson) def upstream = TestingCommons.aggregatedUpstream(api) def reader = new EthereumReader(upstream, caches, TestingCommons.objectMapper()) @@ -167,7 +165,7 @@ class EthereumReaderSpec extends Specification { .setTxByHash(memCache) .setObjectMapper(TestingCommons.objectMapper()) .build() - def reader = new EthereumReader(Stub(Upstream), caches, TestingCommons.objectMapper()) + def reader = new EthereumReader(Stub(Multistream), caches, TestingCommons.objectMapper()) when: def act = reader.txByHash().read(txJson.hash).block() @@ -186,8 +184,7 @@ class EthereumReaderSpec extends Specification { .setObjectMapper(TestingCommons.objectMapper()) .build() - def rpcClient = Stub(ReactorRpcClient) - def api = TestingCommons.api(rpcClient) + def api = TestingCommons.api() api.answer("eth_getTransactionByHash", [txJson.hash.toHex()], txJson) def upstream = TestingCommons.aggregatedUpstream(api) def reader = new EthereumReader(upstream, caches, TestingCommons.objectMapper()) @@ -201,12 +198,12 @@ class EthereumReaderSpec extends Specification { def "Caches balance until block mined"() { setup: - def rpcClient = Stub(ReactorRpcClient) - def api = TestingCommons.api(rpcClient) + def api = TestingCommons.api() api.answerOnce("eth_getBalance", ["0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c", "latest"], "0x10") api.answerOnce("eth_getBalance", ["0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c", "latest"], "0xff") - def upstream = TestingCommons.upstream(api) - def reader = new EthereumReader(upstream, Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper()) + EthereumUpstreamMock upstream = new EthereumUpstreamMock(Chain.ETHEREUM, api) + def upstreams = TestingCommons.aggregatedUpstream(upstream) + def reader = new EthereumReader(upstreams, Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper()) reader.start() when: @@ -229,6 +226,7 @@ class EthereumReaderSpec extends Specification { it.totalDifficulty = BigInteger.TWO } upstream.nextBlock(BlockContainer.from(block2, TestingCommons.objectMapper())) + Thread.sleep(50) act = reader.balance().read(Address.from("0x70b91ff87a902b53dc6e2f6bda8bb9b330ccd30c")).block() then: diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsFactorySpec.groovy similarity index 55% rename from src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsFactorySpec.groovy index c0afb9c5..99b9e0d8 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsFactorySpec.groovy @@ -18,7 +18,6 @@ package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.BlocksMemCache import io.emeraldpay.dshackle.cache.Caches -import io.emeraldpay.dshackle.cache.HeightCache import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.test.TestingCommons @@ -26,7 +25,6 @@ import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.rpc.ReactorRpcClient import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.TransactionRefJson -import io.infinitape.etherjar.rpc.ws.WebsocketClient import reactor.core.publisher.Mono import reactor.test.StepVerifier import spock.lang.Specification @@ -35,44 +33,14 @@ import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit -class EthereumWsSpec extends Specification { +class EthereumWsFactorySpec extends Specification { ObjectMapper objectMapper = TestingCommons.objectMapper() - def "Uses cache to fetch block"() { + def "Fetch block"() { setup: - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - def ws = new EthereumWs(new URI("http://localhost"), new URI("http://localhost"), apiMock, objectMapper) + def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"), objectMapper) def blocksCache = Mock(BlocksMemCache) - def caches = Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).build() - ws.setCaches(caches) - - def block = new BlockJson() - block.number = 100 - block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200") - block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS) - block.totalDifficulty = BigInteger.ONE - - when: - ws.onNewBlock(block) - - then: - 1 * blocksCache.read(BlockId.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")) >> Mono.just(BlockContainer.from(block, objectMapper)) - StepVerifier.create(ws.flux.take(1)) - .expectNext(BlockContainer.from(block, objectMapper)) - .expectComplete() - .verify(Duration.ofSeconds(1)) - } - - def "Fetch block if cache is empty"() { - setup: - ReactorRpcClient rpcClient = Stub(ReactorRpcClient) - def apiMock = TestingCommons.api(rpcClient) - def ws = new EthereumWs(new URI("http://localhost"), new URI("http://localhost"), apiMock, objectMapper) - def blocksCache = Mock(BlocksMemCache) - def caches = Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).build() - ws.setCaches(caches) def block = new BlockJson() block.number = 100 @@ -82,13 +50,16 @@ class EthereumWsSpec extends Specification { block.uncles = [] block.totalDifficulty = BigInteger.ONE + def apiMock = TestingCommons.api() + def upstream = TestingCommons.upstream(apiMock) + def ws = wsf.create(upstream) + apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block) when: ws.onNewBlock(block) then: - 1 * blocksCache.read(_) >> Mono.empty() StepVerifier.create(ws.flux.take(1)) .expectNext(BlockContainer.from(block, objectMapper)) .expectComplete() diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouterSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouterSpec.groovy new file mode 100644 index 00000000..f35c516f --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/NativeCallRouterSpec.groovy @@ -0,0 +1,33 @@ +package io.emeraldpay.dshackle.upstream.ethereum + +import io.emeraldpay.dshackle.cache.Caches +import io.emeraldpay.dshackle.reader.EmptyReader +import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse +import io.emeraldpay.grpc.Chain +import spock.lang.Specification + +import java.time.Duration + +class NativeCallRouterSpec extends Specification { + + def "Calls hardcoded"() { + setup: + def methods = new DefaultEthereumMethods(TestingCommons.objectMapper(), Chain.ETHEREUM) + def router = new NativeCallRouter( + TestingCommons.objectMapper(), + new EthereumReader( + TestingCommons.aggregatedUpstream(TestingCommons.api()), + Caches.default(TestingCommons.objectMapper()), + TestingCommons.objectMapper() + ), + methods + ) + when: + def act = router.read(new JsonRpcRequest("eth_coinbase", [])).block(Duration.ofSeconds(1)) + then: + act.resultAsProcessedString == "0x0000000000000000000000000000000000000000" + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy index 63b95b46..5b236dde 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy @@ -25,11 +25,10 @@ import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.test.MockGrpcServer import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient import io.emeraldpay.grpc.Chain import io.grpc.stub.StreamObserver import io.infinitape.etherjar.domain.BlockHash -import io.infinitape.etherjar.rpc.ReactorRpcClient -import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient import io.infinitape.etherjar.rpc.json.BlockJson import spock.lang.Specification @@ -46,7 +45,7 @@ class EthereumGrpcUpstreamSpec extends Specification { setup: def callData = [:] def chain = Chain.ETHEREUM - def api = TestingCommons.api(Stub(ReactorRpcClient)) + def api = TestingCommons.api() def block1 = new BlockJson().with { it.number = 650246 it.hash = BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7") @@ -73,8 +72,7 @@ class EthereumGrpcUpstreamSpec extends Specification { ) } }) - def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build() - def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, transport) + def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, new JsonRpcGrpcClient(client, chain, objectMapper)) upstream.setLag(0) upstream.init(BlockchainOuterClass.DescribeChain.newBuilder() .addAllSupportedMethods(["eth_getBlockByHash"]) @@ -90,7 +88,7 @@ class EthereumGrpcUpstreamSpec extends Specification { def "Follows difficulty, ignores less difficult"() { setup: - def api = TestingCommons.api(Stub(ReactorRpcClient)) + def api = TestingCommons.api() def block1 = new BlockJson().with { it.number = 650246 it.hash = BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7") @@ -131,8 +129,7 @@ class EthereumGrpcUpstreamSpec extends Specification { ) } }) - def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build() - def upstream = new EthereumGrpcUpstream("test", Chain.ETHEREUM, client, objectMapper, transport) + def upstream = new EthereumGrpcUpstream("test", Chain.ETHEREUM, client, objectMapper, new JsonRpcGrpcClient(client, Chain.ETHEREUM, objectMapper)) upstream.setLag(0) upstream.init(BlockchainOuterClass.DescribeChain.newBuilder() .addAllSupportedMethods(["eth_getBlockByHash"]) @@ -151,7 +148,7 @@ class EthereumGrpcUpstreamSpec extends Specification { def callData = [:] def finished = new CompletableFuture() def chain = Chain.ETHEREUM - def api = TestingCommons.api(Stub(ReactorRpcClient)) + def api = TestingCommons.api() def block1 = new BlockJson().with { it.number = 650246 it.hash = BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7") @@ -193,8 +190,7 @@ class EthereumGrpcUpstreamSpec extends Specification { finished.complete(true) } }) - def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build() - def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, transport) + def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, new JsonRpcGrpcClient(client, chain, objectMapper)) upstream.setLag(0) upstream.init(BlockchainOuterClass.DescribeChain.newBuilder() .addAllSupportedMethods(["eth_getBlockByHash"]) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClientSpec.groovy similarity index 68% rename from src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClientSpec.groovy index 8dd361ab..7ab3ddc6 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClientSpec.groovy @@ -13,22 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream.bitcoin +package io.emeraldpay.dshackle.upstream.rpcclient import io.emeraldpay.dshackle.config.AuthConfig +import io.emeraldpay.dshackle.test.TestingCommons import org.mockserver.integration.ClientAndServer -import org.mockserver.matchers.Times import org.mockserver.model.HttpRequest import org.mockserver.model.HttpResponse import org.mockserver.model.MediaType -import org.mockserver.verify.VerificationTimes import reactor.test.StepVerifier -import spock.lang.Shared import spock.lang.Specification import java.time.Duration -class BitcoinRpcClientSpec extends Specification { +class JsonRpcHttpClientSpec extends Specification { ClientAndServer mockServer @@ -40,39 +38,31 @@ class BitcoinRpcClientSpec extends Specification { mockServer.stop() } - def "Make request"() { + def "Make a request"() { setup: - def client = new BitcoinRpcClient("localhost:18332", null) - + JsonRpcHttpClient client = new JsonRpcHttpClient("localhost:18332", TestingCommons.objectMapper(), null, null) + def resp = '{' + + ' "jsonrpc": "2.0",' + + ' "result": "0x98de45",' + + ' "error": null,' + + ' "id": 15' + + '}' mockServer.when( HttpRequest.request() - .withMethod("POST") - .withBody("ping"), - Times.exactly(1) ).respond( - HttpResponse.response() - .withBody("pong") + HttpResponse.response(resp) ) when: - def act = client.execute("ping".bytes).map { new String(it) } + def act = client.read(new JsonRpcRequest("test", [])).block() then: - StepVerifier.create(act) - .expectNext("pong") - .expectComplete() - .verify(Duration.ofSeconds(1)) - mockServer.verify( - HttpRequest.request() - .withMethod("POST") - .withBody("ping") - .withContentType(MediaType.APPLICATION_JSON) - ) - + act.error == null + new String(act.result) == '"0x98de45"' } def "Make request with basic auth"() { setup: def auth = new AuthConfig.ClientBasicAuth("user", "passwd") - def client = new BitcoinRpcClient("localhost:18332", auth) + def client = new JsonRpcHttpClient("localhost:18332", TestingCommons.objectMapper(), auth, null) mockServer.when( HttpRequest.request() diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParserSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParserSpec.groovy new file mode 100644 index 00000000..0fa9b81d --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParserSpec.groovy @@ -0,0 +1,180 @@ +/** + * 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.rpcclient + +import spock.lang.Specification + +class JsonRpcParserSpec extends Specification { + + JsonRpcParser parser = new JsonRpcParser() + + def "Parse just result"() { + setup: + def json = '{"result": "Hello world!"}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '"Hello world!"' + } + + def "Parse string response"() { + setup: + // 0 8 16 32 35 + def json = '{"jsonrpc": "2.0", "id": 1, "result": "Hello world!"}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '"Hello world!"' + } + + def "Parse string response when result starts first"() { + setup: + // 0 8 16 32 35 + def json = '{"result": "Hello world!", "jsonrpc": "2.0", "id": 1}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '"Hello world!"' + } + + def "Parse bool response"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": false}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == 'false' + } + + def "Parse bool response when id is last"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "result": false, "id": 1}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == 'false' + } + + def "Parse int response"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": 100}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '100' + } + + def "Parse null response"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": null}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == 'null' + } + + def "Parse object response"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": {"hash": "0x00000", "foo": false, "bar": 1}}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '{"hash": "0x00000", "foo": false, "bar": 1}' + } + + def "Parse object response with null error"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": {"hash": "0x00000", "foo": false, "bar": 1}, "error": null}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '{"hash": "0x00000", "foo": false, "bar": 1}' + } + + def "Parse object response if null error comes first"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "error": null, "result": {"hash": "0x00000", "foo": false, "bar": 1}}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '{"hash": "0x00000", "foo": false, "bar": 1}' + } + + def "Parse object response with extra spaces"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "result" : {"hash": "0x00000", "foo": false , "bar":1} , "id": 1}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '{"hash": "0x00000", "foo": false , "bar":1}' + } + + def "Parse complex object response"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": {"hash": "0x00000", "foo": {"bar": 1, "baz": 2}}}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '{"hash": "0x00000", "foo": {"bar": 1, "baz": 2}}' + } + + def "Parse array response"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": [1, 2, false]}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error == null + new String(act.result) == '[1, 2, false]' + } + + def "Parse error"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": null, "error": {"code": -1111, "message": "test"}}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error != null + act.error.code == -1111 + act.error.message == "test" + act.hasError() + !act.hasResult() + } + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequestSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequestSpec.groovy new file mode 100644 index 00000000..0d135a4a --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequestSpec.groovy @@ -0,0 +1,59 @@ +/** + * 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.rpcclient + +import io.emeraldpay.dshackle.test.TestingCommons +import spock.lang.Specification + +class JsonRpcRequestSpec extends Specification { + + def "Serialize empty params"() { + setup: + def req = new JsonRpcRequest("test_foo", []) + when: + def act = req.toJson(TestingCommons.objectMapper()) + then: + new String(act) == '{"jsonrpc":"2.0","id":1,"method":"test_foo","params":[]}' + } + + def "Serialize single param"() { + setup: + def req = new JsonRpcRequest("test_foo", ["0x0000"]) + when: + def act = req.toJson(TestingCommons.objectMapper()) + then: + new String(act) == '{"jsonrpc":"2.0","id":1,"method":"test_foo","params":["0x0000"]}' + } + + def "Serialize two params"() { + setup: + def req = new JsonRpcRequest("test_foo", ["0x0000", false]) + when: + def act = req.toJson(TestingCommons.objectMapper()) + then: + new String(act) == '{"jsonrpc":"2.0","id":1,"method":"test_foo","params":["0x0000",false]}' + } + + def "Same requests are equal"() { + setup: + def req1 = new JsonRpcRequest("test_foo", ["0x0000", false]) + def req2 = new JsonRpcRequest("test_foo", ["0x0000", false]) + when: + def act = req1.equals(req2) + then: + act == true + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponseSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponseSpec.groovy new file mode 100644 index 00000000..fcdcd6a8 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponseSpec.groovy @@ -0,0 +1,59 @@ +/** + * 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.rpcclient + +import spock.lang.Specification + +class JsonRpcResponseSpec extends Specification { + + def "Same responses are equal"() { + setup: + def resp1 = new JsonRpcResponse("\"hello\"".bytes, null) + def resp2 = new JsonRpcResponse("\"hello\"".bytes, null) + when: + def act = resp1.equals(resp2) + then: + act == true + } + + def "Extract processed string without quoted"() { + when: + def act = new JsonRpcResponse("\"hello\"".bytes, null).resultAsProcessedString + then: + act == "hello" + } + + def "Extract raw string with quoted"() { + when: + def act = new JsonRpcResponse("\"hello\"".bytes, null).resultAsRawString + then: + act == "\"hello\"" + } + + def "Fails to extract processed string if not quoted"() { + when: + def act = new JsonRpcResponse("{\"hello\": 1}".bytes, null).resultAsProcessedString + then: + thrown(IllegalStateException) + } + + def "Recognizes null"() { + when: + def act = new JsonRpcResponse("null".bytes, null) + then: + act.isNull() + } +}