solution: refactor and redesign API access, now with AggregateUpstream providing base direct/routed apis, plus actual chain upstreams providing specific readers

This commit is contained in:
Igor Artamonov
2020-05-13 22:29:21 -04:00
parent a539ad3b84
commit 042408b4e6
102 changed files with 1649 additions and 2255 deletions

View File

@@ -93,6 +93,7 @@ class BlocksRedisCache(
Instant.ofEpochMilli(meta.timestamp),
false,
value.value.toByteArray(),
null,
meta.txHashesList.map {
TxId(it.toByteArray())
}

View File

@@ -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<BlockId, BlockContainer> {
return EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash)
return EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash)
}
fun getFullBlocksByHeight(): Reader<Long, BlockContainer> {
return BlockByHeight(blocksByHeight, EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash))
return BlockByHeight(blocksByHeight, EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash))
}
enum class Tag {

View File

@@ -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))

View File

@@ -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) {

View File

@@ -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<TxId> = 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<TransactionJson>()?.count() ?: 0 > 0
return BlockContainer(
block.number,
@@ -43,10 +45,22 @@ 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 equals(other: Any?): Boolean {

View File

@@ -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

View File

@@ -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 <T> getParsed(clazz: Class<T>): 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

View File

@@ -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
)
}
}

View File

@@ -32,13 +32,13 @@ open class AlwaysQuorum: CallQuorum {
return resolved
}
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
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? {

View File

@@ -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<String>(jacksonRpcConverter, String::class.java) {
) : CallQuorum, ValueAwareQuorum<String>(objectMapper, String::class.java) {
private var result: ByteArray? = null
private var txid: String? = null
@@ -40,7 +41,7 @@ open class BroadcastQuorum(
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 +49,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) {

View File

@@ -31,8 +31,8 @@ interface CallQuorum {
fun init(head: Head)
fun isResolved(): Boolean
fun record(response: ByteArray, upstream: Upstream<*>): Boolean
fun record(error: RpcException, upstream: Upstream<*>)
fun record(response: ByteArray, upstream: Upstream): Boolean
fun record(error: RpcException, upstream: Upstream)
fun getResult(): ByteArray?
companion object {
@@ -42,8 +42,8 @@ interface CallQuorum {
}
}
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*>>, CallQuorum> {
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*>>, CallQuorum> { a, b ->
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> {
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { a, b ->
a.record(b.t1, b.t2)
return@BiFunction a
}

View File

@@ -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<Any>(jacksonRpcConverter, Any::class.java) {
) : CallQuorum, ValueAwareQuorum<Any>(objectMapper, Any::class.java) {
private var result: ByteArray? = null
private var tries: Int = 0
@@ -36,7 +37,7 @@ open class NonEmptyQuorum(
return result != null || tries >= maxTries
}
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*>) {
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
tries++
if (responseValue != null) {
result = response
@@ -47,10 +48,10 @@ open class NonEmptyQuorum(
return result
}
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) {
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
}
override fun record(error: RpcException, upstream: Upstream<*>) {
override fun record(error: RpcException, upstream: Upstream) {
}
}

View File

@@ -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<String>(jacksonRpcConverter, String::class.java) {
) : CallQuorum, ValueAwareQuorum<String>(objectMapper, String::class.java) {
private val lock = ReentrantLock()
private var resultValue = 0L
@@ -44,7 +45,7 @@ open class NonceQuorum(
}
}
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) {
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
val value = responseValue?.let { str ->
HexQuantity.from(str).value.toLong()
}
@@ -63,11 +64,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++
}

View File

@@ -32,7 +32,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return result.get() != null
}
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
override fun record(response: ByteArray, upstream: Upstream): Boolean {
val lagging = upstream.getLag() > maxLag
if (!lagging) {
result.set(response)
@@ -41,10 +41,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()
}

View File

@@ -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<T>(
val jacksonRpcConverter: JacksonRpcConverter,
val objectMapper: ObjectMapper,
val clazz: Class<T>
): CallQuorum {
private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java)
fun extractValue(response: ByteArray, clazz: Class<T>): 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<T>(
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)
}

View File

@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.reader
import reactor.core.publisher.Mono
interface Reader<K, D> {
interface Reader<in K, D> {
fun read(key: K): Mono<D>

View File

@@ -19,11 +19,12 @@ package io.emeraldpay.dshackle.rpc
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.BlockchainType
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.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.RpcException
import org.apache.commons.lang3.StringUtils
@@ -46,11 +47,11 @@ open class NativeCall(
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<RawCallDetails>): CallContext<ParsedCallDetails> {
@@ -97,7 +98,7 @@ open class NativeCall(
return prepareCall(request, upstream)
}
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<*>): Flux<CallContext<RawCallDetails>> {
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux<CallContext<RawCallDetails>> {
return request.itemsList.toFlux().map {
val method = it.method
val params = it.payload.toStringUtf8()
@@ -115,29 +116,26 @@ open class NativeCall(
}
fun fetch(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
return fetchFromCache(ctx)
.onErrorResume { t ->
log.warn("Failed to read from cache", t);
Mono.empty()
}
.switchIfEmpty(
Mono.just(ctx).flatMap(this::executeOnRemote)
)
}
fun fetchFromCache(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
val cachingApi = ctx.upstream.cache
return cachingApi.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { ctx.withPayload(it) }
// ctx.upstream.getRoutedApi(ctx.matcher)
// .flatMap { api ->
// api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
// }.switchIfEmpty(
// Mono.just(ctx).flatMap(this::executeOnRemote)
// )
//TODO use routed api
return executeOnRemote(ctx)
}
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
//TODO move to routed api
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)
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
.flatMap(JsonRpcResponse::requireResult)
// on error notify quorum, it may use error message or other details
.doOnError { err ->
if (err is RpcException) {
@@ -204,7 +202,7 @@ open class NativeCall(
}
open class CallContext<T>(val id: Int,
val upstream: AggregatedUpstream<*>,
val upstream: AggregatedUpstream,
val matcher: Selector.Matcher,
val callQuorum: CallQuorum,
val payload: T) {
@@ -212,8 +210,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)
}
}

View File

@@ -64,7 +64,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()

View File

@@ -46,7 +46,7 @@ class SubscribeStatus(
}
}
fun chainStatus(chain: Chain, ups: List<Upstream<*>>): BlockchainOuterClass.ChainStatus {
fun chainStatus(chain: Chain, ups: List<Upstream>): 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: AggregatedUpstream, val avail: UpstreamAvailability)
}

View File

@@ -19,9 +19,9 @@ 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.BitcoinChainUpstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
@@ -63,8 +63,8 @@ class TrackBitcoinAddress(
}
}
fun requestBalances(chain: Chain, api: DirectBitcoinApi, addresses: List<String>): Flux<AddressBalance> {
return api.executeAndResult(0, "listunspent", emptyList(), List::class.java)
fun requestBalances(chain: Chain, api: BitcoinChainUpstreams, addresses: List<String>): Flux<AddressBalance> {
return api.getReader().listUnspent()
.flatMapMany { unspents ->
val result = getTotal(chain, addresses, unspents)
Flux.fromIterable(result)
@@ -73,17 +73,14 @@ class TrackBitcoinAddress(
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
val upstream = upstreams.getUpstream(chain)?.cast(BitcoinChainUpstreams::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<String>, unspents: List<*>): List<AddressBalance> {
@@ -122,20 +119,18 @@ class TrackBitcoinAddress(
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
println("up: ${upstreams.getUpstream(chain)}")
println("up cast: ${upstreams.getUpstream(chain)?.cast(BitcoinChainUpstreams::class.java)}")
val upstream = upstreams.getUpstream(chain)?.cast(BitcoinChainUpstreams::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<Address, BigInteger>()
val result = Flux.merge(initial, following)

View File

@@ -20,9 +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.BitcoinChainUpstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
import io.emeraldpay.grpc.Chain
@@ -52,31 +51,30 @@ class TrackBitcoinTx(
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
val chain = Chain.byId(request.chainValue)
val upstream = upstreams.getUpstream(chain)?.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
val upstream = upstreams.getUpstream(chain)?.cast(BitcoinChainUpstreams::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<TxStatus> {
return loadExisting(api, txid)
fun subscribe(chain: Chain, upstream: BitcoinChainUpstreams, txid: String): Flux<TxStatus> {
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 +83,8 @@ class TrackBitcoinTx(
}
}
fun continueWithMined(api: DirectBitcoinApi, upstream: BitcoinUpstream, status: TxStatus): Flux<TxStatus> {
return api.getBlock(status.blockHash!!)
fun continueWithMined(upstream: BitcoinChainUpstreams, status: TxStatus): Flux<TxStatus> {
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 +92,40 @@ class TrackBitcoinTx(
}
}
fun untilFound(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> {
fun untilFound(chain: Chain, upstream: BitcoinChainUpstreams, txid: String): Flux<TxStatus> {
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<TxStatus> {
fun untilMined(upstream: BitcoinChainUpstreams, tx: TxStatus): Mono<TxStatus> {
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<TxStatus> {
fun withConfirmations(upstream: BitcoinChainUpstreams, tx: TxStatus): Flux<TxStatus> {
return upstream.getHead().getFlux().map {
tx.withHead(it.height)
}
}
fun loadExisting(api: DirectBitcoinApi, txid: String): Mono<TxStatus> {
val mined = api.getTx(txid)
fun loadExisting(api: BitcoinChainUpstreams, txid: String): Mono<TxStatus> {
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<TxStatus> {
val mempool = upstream.getData().getMempool().get()
fun loadMempool(upstream: BitcoinChainUpstreams, txid: String): Mono<TxStatus> {
val mempool = upstream.getReader().getMempool().get()
return mempool.map {
if (it.contains(txid)) {
TxStatus(txid, found = true, mined = false)

View File

@@ -22,8 +22,7 @@ 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.ethereum.EthereumChainUpstream
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.Wei
@@ -87,8 +86,8 @@ class TrackEthereumAddress(
}
}
fun getUpstream(chain: Chain): AggregatedEthereumUpstreams {
return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java)
fun getUpstream(chain: Chain): EthereumChainUpstream {
return upstreams.getUpstream(chain)?.cast(EthereumChainUpstream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain)
}

View File

@@ -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.ethereum.EthereumChainUpstream
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId
@@ -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): EthereumChainUpstream {
return upstreams.getUpstream(chain)?.cast(EthereumChainUpstream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain)
}
fun subscribe(base: TxDetails, up: AggregatedEthereumUpstreams): Flux<TxDetails> {
fun subscribe(base: TxDetails, up: EthereumChainUpstream): Flux<TxDetails> {
var latestTx = base
val untilFound = Mono.just(latestTx)
@@ -215,7 +213,7 @@ class TrackEthereumTx(
}
}
fun updateFromBlock(upstream: AggregatedEthereumUpstreams, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> {
fun updateFromBlock(upstream: EthereumChainUpstream, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> {
return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) {
val updated = tx.withStatus(
blockHash = blockTx.blockHash,

View File

@@ -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.reader.Reader
import io.emeraldpay.dshackle.upstream.CurrentUpstreams
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient
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
@@ -46,7 +47,8 @@ open class ConfiguredUpstreams(
@Autowired private val objectMapper: ObjectMapper,
@Autowired private val currentUpstreams: CurrentUpstreams,
@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<JsonRpcRequest, JsonRpcResponse>? = 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<UpstreamsConfig.EthereumConnection>,
chain: Chain,
options: UpstreamsConfig.Options) {
val conn = config.connection!!
var rpcApi: DirectEthereumApi? = null
val directApi: Reader<JsonRpcRequest, JsonRpcResponse>? = buildHttpClient(config)
if (directApi == null) {
log.warn("Upstream doesn't have API configuration")
return
}
val urls = ArrayList<URI>()
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<UpstreamsConfig.GrpcConnection>, options: UpstreamsConfig.Options) {
@@ -218,7 +201,8 @@ open class ConfiguredUpstreams(
endpoint.port ?: 2449,
objectMapper,
endpoint.auth,
fileResolver
fileResolver,
cachesFactory
).apply {
timeout = options.timeout
}
@@ -231,4 +215,22 @@ open class ConfiguredUpstreams(
}
private fun buildHttpClient(config: UpstreamsConfig.Upstream<out UpstreamsConfig.RpcConnection>): JsonRpcHttpClient? {
val conn = config.connection!!
val urls = ArrayList<URI>()
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
)
}
}
}

View File

@@ -32,7 +32,7 @@ class UpstreamChange(
/**
* Corresponding upstream
*/
val upstream: Upstream<*>,
val upstream: Upstream,
/**
* Type of the change
*/

View File

@@ -19,11 +19,15 @@ 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.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 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
@@ -34,19 +38,42 @@ import kotlin.concurrent.withLock
/**
* Aggregation of multiple upstreams responding to a single blockchain
*/
abstract class AggregatedUpstream<U : UpstreamApi>(
private val objectMapper: ObjectMapper,
abstract class AggregatedUpstream(
val caches: Caches
) : Upstream<U>, Lifecycle {
) : 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<Upstream<U>>
abstract fun addUpstream(upstream: Upstream<U>)
abstract fun getApis(matcher: Selector.Matcher): ApiSource<U>
/**
* Get list of all underlying upstreams
*/
abstract fun getAll(): List<Upstream>
/**
* Add an upstream
*/
abstract fun addUpstream(upstream: Upstream)
/**
* Get a source for direct APIs
*/
abstract fun getApiSource(matcher: Selector.Matcher): ApiSource
/**
* Finds an API that executed directly on a remote.
*/
abstract fun getDirectApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>>
/**
* Finds an API that leverages caches and other optimizations/transformations of the request.
*/
abstract fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>>
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
throw NotImplementedError("Immediate direct API is not implemented for Aggregated Upstream")
}
fun onUpstreamsUpdated() {
reconfigLock.withLock {
@@ -95,13 +122,12 @@ abstract class AggregatedUpstream<U : UpstreamApi>(
cacheSubscription = head.getFlux().subscribe {
caches.cache(Caches.Tag.LATEST, it)
}
cache = CachingEthereumApi(objectMapper, caches, head)
}
}
// --------------------------------------------------------------------------------------------------------
class UpstreamStatus(val upstream: Upstream<UpstreamApi>, val status: UpstreamAvailability, val ts: Instant = Instant.now())
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
class FilterBestAvailability() : Predicate<UpstreamStatus> {
private val lastRef = AtomicReference<UpstreamStatus>()

View File

@@ -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<U : UpstreamApi> : Publisher<U> {
interface ApiSource : Publisher<Reader<JsonRpcRequest, JsonRpcResponse>> {
fun resolve()
fun request(tries: Int)

View File

@@ -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<Any>): Mono<ByteArray> {
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<Any>): Mono<ByteArray> {
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<Any>): Mono<ByteArray> {
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<in Mono<out SourceContainer>, out Mono<ByteArray>> {
return Function { mono ->
mono.map(containerToJson(id))
}
}
/**
* Handle errors and other stuff
*/
fun finalizer(): Function<Mono<ByteArray>, Mono<ByteArray>> {
return Function { mono ->
mono.onErrorResume { t ->
log.warn("Error during read from cache", t)
Mono.empty()
}
}
}
fun bytesToJson(id: Int): Function<ByteArray, ByteArray> {
return Function { data ->
rawJsonBuilder.write(id, data)
}
}
fun containerToJson(id: Int): Function<SourceContainer, ByteArray> {
return Function { data ->
rawJsonBuilder.write(id, data.json!!)
}
}
}

View File

@@ -16,8 +16,10 @@
*/
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.reader.Reader
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,16 +31,15 @@ import java.time.Duration
/**
* General interface to upstream(s) to a single chain
*/
abstract class ChainUpstreams<U : UpstreamApi>(
abstract class ChainUpstreams(
val chain: Chain,
private val upstreams: MutableList<Upstream<U>>,
caches: Caches,
objectMapper: ObjectMapper
) : AggregatedUpstream<U>(objectMapper, caches), Lifecycle {
private val upstreams: MutableList<Upstream>,
caches: Caches
) : AggregatedUpstream(caches), Lifecycle {
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
private var seq = 0
protected var lagObserver: HeadLagObserver<U>? = null
protected var lagObserver: HeadLagObserver? = null
private var subscription: Disposable? = null
open fun init() {
@@ -75,11 +76,11 @@ abstract class ChainUpstreams<U : UpstreamApi>(
lagObserver?.stop()
}
override fun getAll(): List<Upstream<U>> {
override fun getAll(): List<Upstream> {
return upstreams
}
override fun addUpstream(upstream: Upstream<U>) {
override fun addUpstream(upstream: Upstream) {
upstreams.add(upstream)
setHead(updateHead())
onUpstreamsUpdated()
@@ -92,7 +93,7 @@ abstract class ChainUpstreams<U : UpstreamApi>(
}
}
override fun getApis(matcher: Selector.Matcher): ApiSource<U> {
override fun getApiSource(matcher: Selector.Matcher): ApiSource {
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
@@ -100,11 +101,11 @@ abstract class ChainUpstreams<U : UpstreamApi>(
return FilteredApis(upstreams, matcher, i)
}
override fun getApi(matcher: Selector.Matcher): Mono<U> {
val apis = getApis(matcher)
override fun getDirectApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
val apis = getApiSource(matcher)
apis.request(1)
return Mono.from(apis)
.switchIfEmpty(Mono.error<U>(Exception("No API available")))
.switchIfEmpty(Mono.error(Exception("No API available")))
}
override fun setLag(lag: Long) {

View File

@@ -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.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.EthereumChainUpstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
@@ -51,7 +49,7 @@ class CurrentUpstreams(
private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java)
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams<*>>()
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams>()
private val chainsBus = TopicProcessor.create<Chain>()
private val callTargets = HashMap<Chain, CallMethods>()
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<EthereumApi>
val current = chainMapping[chain] as ChainUpstreams<EthereumApi>?
val up = change.upstream.cast(EthereumUpstream::class.java)
val current = chainMapping[chain] as ChainUpstreams?
val factory = Callable {
AggregatedEthereumUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<EthereumApi>
EthereumChainUpstream(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams
}
processUpdate(change, up, current, factory)
}
BlockchainType.BITCOIN -> {
val up = change.upstream
.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
val current = chainMapping[chain] as ChainUpstreams<DirectBitcoinApi>?
val up = change.upstream.cast(BitcoinUpstream::class.java)
val current = chainMapping[chain] as ChainUpstreams?
val factory = Callable {
BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<DirectBitcoinApi>
BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams
}
processUpdate(change, up, current, factory)
}
@@ -85,7 +81,7 @@ class CurrentUpstreams(
}
}
fun <A : UpstreamApi> processUpdate(change: UpstreamChange, up: Upstream<A>, current: ChainUpstreams<A>?, factory: Callable<ChainUpstreams<A>>) {
fun processUpdate(change: UpstreamChange, up: Upstream, current: ChainUpstreams?, factory: Callable<ChainUpstreams>) {
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): AggregatedUpstream? {
return chainMapping[chain]
}

View File

@@ -22,13 +22,13 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
import java.util.concurrent.atomic.AtomicReference
abstract class DefaultUpstream<U : UpstreamApi>(
abstract class DefaultUpstream(
private val id: String,
defaultLag: Long,
defaultAvail: UpstreamAvailability,
private val options: UpstreamsConfig.Options,
private val targets: CallMethods?
) : Upstream<U> {
) : Upstream {
constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets)

View File

@@ -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<U : UpstreamApi>(
allUpstreams: List<Upstream<U>>,
class FilteredApis(
allUpstreams: List<Upstream>,
private val matcher: Selector.Matcher,
pos: Int,
private val repeatLimit: Long,
jitter: Int
) : ApiSource<U> {
) : ApiSource {
companion object {
private const val DEFAULT_DELAY_STEP = 100
private const val MAX_WAIT_MILLIS = 5000L
}
constructor(allUpstreams: List<Upstream<U>>,
constructor(allUpstreams: List<Upstream>,
matcher: Selector.Matcher,
pos: Int) : this(allUpstreams, matcher, pos, 10, 7)
constructor(allUpstreams: List<Upstream<U>>,
constructor(allUpstreams: List<Upstream>,
matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
private val delay: Int
private val upstreams: List<Upstream<UpstreamApi>>
private val upstreams: List<Upstream>
private val control = EmitterProcessor.create<Boolean>(32, false)
@@ -75,18 +78,19 @@ class FilteredApis<U : UpstreamApi>(
return Duration.ofMillis(time)
}
override fun subscribe(subscriber: Subscriber<in U>) {
override fun subscribe(subscriber: Subscriber<in Reader<JsonRpcRequest, JsonRpcResponse>>) {
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<UpstreamApi>::isAvailable)
.filter(Upstream::isAvailable)
.filter(matcher::matches)
.flatMap { it.getApi(matcher) }
.zipWith(control).map { it.t1 }
.subscribe(subscriber as Subscriber<in UpstreamApi>)
.map { it.getApi() }
.zipWith(control)
.map { it.t1 }
.subscribe(subscriber)
}
override fun resolve() {

View File

@@ -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<A : UpstreamApi>(
abstract class HeadLagObserver(
private val master: Head,
private val followers: Collection<Upstream<A>>
private val followers: Collection<Upstream>
) : Lifecycle {
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
@@ -58,7 +58,7 @@ abstract class HeadLagObserver<A : UpstreamApi>(
}
}
fun probeFollowers(top: BlockContainer): Flux<Tuple2<Long, Upstream<A>>> {
fun probeFollowers(top: BlockContainer): Flux<Tuple2<Long, Upstream>> {
return Flux.fromIterable(followers)
.parallel(followers.size)
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
@@ -66,9 +66,9 @@ abstract class HeadLagObserver<A : UpstreamApi>(
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
}
abstract fun getCurrentBlocks(up: Upstream<A>): Flux<BlockContainer>
abstract fun getCurrentBlocks(up: Upstream): Flux<BlockContainer>
fun mapLagging(top: BlockContainer, up: Upstream<A>, blocks: Flux<BlockContainer>): Flux<Tuple2<Long, Upstream<A>>> {
fun mapLagging(top: BlockContainer, up: Upstream, blocks: Flux<BlockContainer>): Flux<Tuple2<Long, Upstream>> {
return blocks
.map { extractDistance(top, it) }
.takeUntil { lag -> lag <= 0L }

View File

@@ -96,13 +96,13 @@ class Selector {
}
interface Matcher {
fun matches(up: Upstream<UpstreamApi>): Boolean
fun matches(up: Upstream): Boolean
}
class MultiMatcher(
private val matchers: Collection<Matcher>
): Matcher {
override fun matches(up: Upstream<UpstreamApi>): 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<UpstreamApi>): Boolean {
override fun matches(up: Upstream): Boolean {
return up.getMethods().isAllowed(method)
}
}
abstract class LabelSelectorMatcher: Matcher {
override fun matches(up: Upstream<UpstreamApi>): 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<UpstreamApi>): Boolean {
override fun matches(up: Upstream): Boolean {
return true
}
}
@@ -144,7 +144,7 @@ class Selector {
return null
}
override fun matches(up: Upstream<UpstreamApi>): Boolean {
override fun matches(up: Upstream): Boolean {
return true
}
}

View File

@@ -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<out A : UpstreamApi> {
interface Upstream {
fun isAvailable(): Boolean
fun getStatus(): UpstreamAvailability
fun observeStatus(): Flux<UpstreamAvailability>
fun getHead(): Head
fun getApi(matcher: Selector.Matcher): Mono<out A>
fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse>
fun getOptions(): UpstreamsConfig.Options
fun setLag(lag: Long)
fun getLag(): Long
@@ -34,6 +37,5 @@ interface Upstream<out A : UpstreamApi> {
fun getMethods(): CallMethods
fun getId(): String
fun <TA : UpstreamApi> castApi(apiType: Class<TA>): Upstream<TA>
fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T
fun <T : Upstream> cast(selfType: Class<T>): T
}

View File

@@ -16,12 +16,16 @@
*/
package io.emeraldpay.dshackle.upstream
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.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
interface Upstreams {
fun getUpstream(chain: Chain): AggregatedUpstream<*>?
fun getUpstream(chain: Chain): AggregatedUpstream?
fun getAvailable(): List<Chain>
fun observeChains(): Flux<Chain>
fun getDefaultMethods(chain: Chain): CallMethods

View File

@@ -18,18 +18,21 @@ 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.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 BitcoinChainUpstreams(
chain: Chain,
val upstreams: MutableList<BitcoinUpstream>,
caches: Caches,
objectMapper: ObjectMapper
) : ChainUpstreams<DirectBitcoinApi>(chain, upstreams as MutableList<Upstream<DirectBitcoinApi>>, caches, objectMapper) {
private val objectMapper: ObjectMapper
) : ChainUpstreams(chain, upstreams as MutableList<Upstream>, caches), Lifecycle {
companion object {
private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java)
@@ -37,6 +40,9 @@ class BitcoinChainUpstreams(
private var head: Head? = null
//TODO head
private var reader = BitcoinReader(this, EmptyHead(), objectMapper)
override fun init() {
if (upstreams.size > 0) {
head = updateHead()
@@ -68,8 +74,18 @@ class BitcoinChainUpstreams(
return head
}
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
//TODO
return getDirectApi(matcher)
}
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 +96,24 @@ class BitcoinChainUpstreams(
return upstreams.flatMap { it.getLabels() }
}
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as Upstream<A>
}
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
override fun <T : Upstream> cast(selfType: Class<T>): 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()
}
}

View File

@@ -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: BitcoinChainUpstreams,
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<Map<String, Any>> {
return castedRead(JsonRpcRequest("getblock", listOf(hash)), Map::class.java).cast()
}
open fun getTx(txid: String): Mono<Map<String, Any>> {
return castedRead(JsonRpcRequest("getrawtransaction", listOf(txid, true)), Map::class.java).cast()
}
open fun listUnspent(): Mono<List<String>> {
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 <T> castedRead(req: JsonRpcRequest, clazz: Class<T>): Mono<T> {
return upstreams.getDirectApi(Selector.empty).flatMap { api ->
api.read(req)
.flatMap(JsonRpcResponse::requireResult)
.map {
objectMapper.readValue(it, clazz) as T
}
}
}
}

View File

@@ -1,69 +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.config.AuthConfig
import io.netty.buffer.Unpooled
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaders
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import reactor.netty.http.client.HttpClient
import java.util.*
import java.util.function.Consumer
open class BitcoinRpcClient(
private val target: String,
basicAuth: AuthConfig.ClientBasicAuth?
) {
companion object {
private val log = LoggerFactory.getLogger(BitcoinRpcClient::class.java)
}
private val httpClient: HttpClient
init {
var build = HttpClient.create()
build = build.headers { h ->
h.add(HttpHeaderNames.CONTENT_TYPE, "application/json")
}
basicAuth?.let { basicAuth ->
val authString: String = basicAuth.username + ":" + basicAuth.password
val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray())
val auth = "Basic $authBase64"
val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, auth) }
build = build.headers(headers)
}
this.httpClient = build
}
fun execute(request: ByteArray): Mono<ByteArray> {
val response = httpClient
.post()
.uri(target)
.send(Mono.just(request).map { Unpooled.wrappedBuffer(it) })
return response.responseContent()
.aggregate()
.asByteArray()
}
}

View File

@@ -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<JsonRpcRequest, JsonRpcResponse>,
private val extractBlock: ExtractBlock,
private val interval: Duration = Duration.ofSeconds(15)
) : Head, AbstractHead(), Lifecycle {
@@ -53,16 +56,19 @@ 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")))
}
.onErrorContinue { err, _ ->
err.printStackTrace()
log.debug("RPC error ${err.message}")
}
refreshSubscription = super.follow(base)

View File

@@ -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<JsonRpcRequest, JsonRpcResponse>,
options: UpstreamsConfig.Options,
val node: QuorumForLabels.QuorumItem,
private val objectMapper: ObjectMapper,
callMethods: CallMethods
) : DefaultUpstream<DirectBitcoinApi>(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<out DirectBitcoinApi> {
return Mono.just(api)
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
return directApi
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return listOf(UpstreamsConfig.Labels())
}
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
return castApi(apiType) as T
}
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
throw ClassCastException("Cannot cast ${DirectBitcoinApi::class.java} to $apiType")
}
return this as Upstream<A>
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()
}
}

View File

@@ -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<JsonRpcRequest, JsonRpcResponse>,
private val options: UpstreamsConfig.Options
) {
@@ -36,7 +39,9 @@ class BitcoinUpstreamValidator(
}
fun validate(): Mono<UpstreamAvailability> {
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) {

View File

@@ -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: BitcoinChainUpstreams,
private val head: Head,
private val objectMapper: ObjectMapper
) : Lifecycle {
companion object {
@@ -56,7 +61,11 @@ open class CachingMempoolData(
}
fun fetchFromUpstream(): Mono<List<String>> {
return api.executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono<List<String>>
return upstreams.getDirectApi(Selector.empty).flatMap { api ->
api.read(JsonRpcRequest("getrawmempool", emptyList()))
.flatMap(JsonRpcResponse::requireResult)
.map { objectMapper.readValue(it, List::class.java) as List<String> }
}
}
class Container(val since: Instant, val value: List<String>) {

View File

@@ -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<Any>): Mono<ByteArray> {
//TODO it's almost the same code as for DirectEthereumApi; refactor
val result: Mono<out Any> = 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<out Any>): Mono<ByteArray> {
//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<Any, Int>()
resp.id = id
resp.result = it
resp
}
.switchIfEmpty(
Mono.fromCallable {
val resp = ResponseJson<Any, Int>()
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<ByteArray>()
} 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<Any, Int>()
resp.id = id
resp.error = t.error
Mono.just(objectMapper.writer().writeValueAsBytes(resp))
}
}
open fun <T> executeAndResult(id: Int, method: String, params: List<Any>, resultType: Class<T>): Mono<T> {
val rpc = RequestJson<Int>(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<FullResponseJson<T, Int>>(json)
if (resp.hasError()) {
Mono.error(resp.error.asException())
} else {
Mono.just(resp.result)
}
}
}
open fun getBlock(hash: String): Mono<Map<String, Any>> {
return executeAndResult(0, "getblock", listOf(hash), Map::class.java) as Mono<Map<String, Any>>
}
open fun getTx(txid: String): Mono<Map<String, Any>> {
return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono<Map<String, Any>>
}
}

View File

@@ -63,6 +63,7 @@ class ExtractBlock(
getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"),
false,
json,
data,
transactions
)
}

View File

@@ -68,7 +68,7 @@ class AggregatedCallMethods(
/**
* 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)
}?.executeHardcoded(method) ?: throw IllegalStateException("No hardcoded for $method")

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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<String>()
}
throw RpcException(-32601, "Method not found")
return json.toByteArray()
}
override fun getSupportedMethods(): Set<String> {

View File

@@ -44,7 +44,7 @@ open class DirectCallMethods(private val methods: Set<String>) : CallMethods {
return false
}
override fun executeHardcoded(method: String): Any {
return "unsupported"
override fun executeHardcoded(method: String): ByteArray {
return "unsupported".toByteArray()
}
}

View File

@@ -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)
}
}

View File

@@ -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<Any>): Mono<ByteArray> {
val result: Mono<out Any> = 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<out Any>): Mono<ByteArray> {
return result
.doOnError { t ->
log.warn("Upstream error: [${t.message}] for $method")
}
.map {
val resp = ResponseJson<Any, Int>()
resp.id = id
resp.result = it
resp
}
.switchIfEmpty(
Mono.fromCallable {
val resp = ResponseJson<Any, Int>()
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<ByteArray>()
} 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<Any, Int>()
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<Any>): Mono<out Any> {
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<Any>): RpcCall<out Any, out Any> {
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)
}
}
}

View File

@@ -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<EthereumApi>? = null
fun <JS, RS> reader(): Reader<RpcCall<JS, RS>, RS> {
return object : Reader<RpcCall<JS, RS>, RS> {
override fun read(key: RpcCall<JS, RS>): Mono<RS> {
return this@EthereumApi.executeAndConvert(key)
}
}
}
fun <JS, RS> execute(rpcCall: RpcCall<JS, RS>): Mono<ByteArray> {
return execute(0, rpcCall.method, rpcCall.params as List<Any>)
}
fun <JS, RS> executeAndConvert(rpcCall: RpcCall<JS, RS>): Mono<RS> {
val convertToJS = java.util.function.Function<ByteArray, Mono<JS>> { resp ->
val inputStream: InputStream = resp.inputStream()
val jsonValue: JS? = jacksonRpcConverter.fromJson(inputStream, rpcCall.jsonType, Int::class.java)
if (jsonValue == null) Mono.empty<JS>()
else Mono.just(jsonValue)
}
return execute(rpcCall)
.flatMap(convertToJS)
.map(rpcCall.converter::apply)
.doOnError { err -> log.debug("Failed to read from upstream", err) }
}
}

View File

@@ -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 EthereumChainUpstream(
chain: Chain,
val upstreams: MutableList<EthereumUpstream>,
caches: Caches,
objectMapper: ObjectMapper
) : ChainUpstreams<EthereumApi>(chain, upstreams as MutableList<Upstream<EthereumApi>>, caches, objectMapper) {
private val objectMapper: ObjectMapper
) : ChainUpstreams(chain, upstreams as MutableList<Upstream>, caches) {
companion object {
private val log = LoggerFactory.getLogger(AggregatedEthereumUpstreams::class.java)
private val log = LoggerFactory.getLogger(EthereumChainUpstream::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<Upstream<EthereumApi>>).apply {
val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection<Upstream>).apply {
this.start()
}
this.lagObserver = lagObserver
@@ -107,18 +111,17 @@ open class AggregatedEthereumUpstreams(
}
@SuppressWarnings("unchecked")
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
override fun <T : Upstream> cast(selfType: Class<T>): 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 <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
return getDirectApi(matcher).map { api ->
NativeCallRouter(objectMapper, reader, api, getMethods())
}
return this as Upstream<A>
}
}

View File

@@ -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<BlockId, BlockContainer>,
private val txes: Reader<TxId, TxContainer>
) : Reader<BlockId, BlockContainer> {
companion object {
private val log = LoggerFactory.getLogger(EthereumBlocksWithTxCache::class.java)
private val log = LoggerFactory.getLogger(EthereumFullBlocksReader::class.java)
}
override fun read(key: BlockId): Mono<BlockContainer> {

View File

@@ -26,14 +26,14 @@ import java.time.Duration
class EthereumHeadLagObserver(
master: Head,
followers: Collection<Upstream<EthereumApi>>
) : HeadLagObserver<EthereumApi>(master, followers) {
followers: Collection<Upstream>
) : HeadLagObserver(master, followers) {
companion object {
private val log = LoggerFactory.getLogger(EthereumHeadLagObserver::class.java)
}
override fun getCurrentBlocks(up: Upstream<EthereumApi>): Flux<BlockContainer> {
override fun getCurrentBlocks(up: Upstream): Flux<BlockContainer> {
val head = up.getHead()
return head.getFlux().take(Duration.ofSeconds(1))
}

View File

@@ -18,22 +18,22 @@ 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.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams
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
@@ -48,7 +48,7 @@ import java.util.concurrent.TimeoutException
import java.util.function.Function
open class EthereumReader(
private val up: Upstream<EthereumApi>,
private val up: AggregatedUpstream,
private val caches: Caches,
private val objectMapper: ObjectMapper
) : Lifecycle {
@@ -60,48 +60,107 @@ open class EthereumReader(
private var headListener: Disposable? = null
private val balanceCache = CurrentBlockCache<Address, Wei>()
private val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { block ->
objectMapper
.readValue(block.json, BlockJson::class.java)
.withoutTransactionDetails()
val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { 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<TxContainer, TransactionJson> { tx ->
objectMapper
.readValue(tx.json, TransactionJson::class.java)
val extractTx = Function<TxContainer, TransactionJson> { tx ->
tx.getParsed(TransactionJson::class.java) ?: objectMapper.readValue(tx.json, TransactionJson::class.java)
}
private val blocksDirect: Reader<BlockHash, BlockJson<TransactionRefJson>>
private val txDirect: Reader<TransactionId, TransactionJson>
val asRaw = Function<SourceContainer, ByteArray> { tx ->
tx.json ?: ByteArray(0)
}
val jsonToRaw = Function<Any, ByteArray> { json ->
objectMapper.writeValueAsBytes(json)
}
val blockAsContainer = Function<BlockJson<*>, BlockContainer> { block ->
BlockContainer.from(block.withoutTransactionDetails(), objectMapper)
}
val txAsContainer = Function<TransactionJson, TxContainer> { tx ->
TxContainer.from(tx, objectMapper)
}
private val blocksDirect: Reader<BlockHash, BlockContainer>
private val blocksByHeightDirect: Reader<Long, BlockContainer>
private val txDirect: Reader<TransactionId, TxContainer>
private val balanceDirect: Reader<Address, Wei>
private val idToBlockHash = Function<BlockId, BlockHash> { id -> BlockHash.from(id.value) }
private val blockHashToId = Function<BlockHash, BlockId> { hash -> BlockId.from(hash) }
private val txHashToId = Function<TransactionId, TxId> { hash -> TxId.from(hash) }
private val idToTxHash = Function<TxId, TransactionId> { id -> TransactionId.from(id.value) }
private val directResponseBytes = Function<JsonRpcResponse, ByteArray> { resp ->
if (resp.error != null) {
throw resp.error.asException()
} else {
resp.getResult()
}
}
init {
blocksDirect = object : Reader<BlockHash, BlockJson<TransactionRefJson>> {
override fun read(key: BlockHash): Mono<BlockJson<TransactionRefJson>> {
return up.getApi(Selector.empty).flatMap { api ->
api.executeAndConvert(Commands.eth().getBlock(key))
blocksDirect = object : Reader<BlockHash, BlockContainer> {
override fun read(key: BlockHash): Mono<BlockContainer> {
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<TransactionRefJson>
BlockContainer.from(block, blockbytes)
}
.doOnNext { block ->
caches.cache(Caches.Tag.REQUESTED, BlockContainer.from(block, objectMapper))
caches.cache(Caches.Tag.REQUESTED, block)
}
}
}
}
txDirect = object : Reader<TransactionId, TransactionJson> {
override fun read(key: TransactionId): Mono<TransactionJson> {
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<Long, BlockContainer> {
override fun read(key: Long): Mono<BlockContainer> {
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<TransactionRefJson>
BlockContainer.from(block, blockbytes)
}
.doOnNext { block ->
caches.cache(Caches.Tag.REQUESTED, block)
}
}
}
}
txDirect = object : Reader<TransactionId, TxContainer> {
override fun read(key: TransactionId): Mono<TxContainer> {
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 +168,19 @@ open class EthereumReader(
}
balanceDirect = object : Reader<Address, Wei> {
override fun read(key: Address): Mono<Wei> {
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 +190,61 @@ open class EthereumReader(
}
}
fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> {
return CompoundReader(
TransformingReader(caches.getBlocksByHash(), extractBlock),
RekeyingReader(idToBlockHash, blocksDirect)
fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
return TransformingReader(
CompoundReader(
RekeyingReader(blockHashToId, caches.getBlocksByHash()),
blocksDirect
),
extractBlock
)
}
fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
return CompoundReader(
RekeyingReader(
blockHashToId,
TransformingReader(caches.getBlocksByHash(), extractBlock)
fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> {
return TransformingReader(
CompoundReader(
caches.getBlocksByHash(),
RekeyingReader(idToBlockHash, blocksDirect)
),
blocksDirect
extractBlock
)
}
fun blocksByHashAsCont(): Reader<BlockHash, BlockContainer> {
return TransformingReader(
blocksByHash(),
blockAsContainer
)
}
fun blocksByIdAsCont(): Reader<BlockId, BlockContainer> {
return TransformingReader(
blocksById(),
blockAsContainer
)
}
fun blocksByHeightAsCont(): Reader<Long, BlockContainer> {
return CompoundReader(
caches.getBlocksByHeight(),
blocksByHeightDirect
)
}
fun txByHash(): Reader<TransactionId, TransactionJson> {
return CompoundReader(
RekeyingReader(
txHashToId,
TransformingReader(caches.getTxByHash(), extractTx)
return TransformingReader(
CompoundReader(
RekeyingReader(txHashToId, caches.getTxByHash()),
txDirect
),
txDirect
extractTx
)
}
fun txByHashAsCont(): Reader<TxId, TxContainer> {
return CompoundReader(
caches.getTxByHash(),
RekeyingReader(idToTxHash, txDirect)
)
}

View File

@@ -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<in JsonRpcRequest, out JsonRpcResponse>,
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}")

View File

@@ -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<JsonRpcRequest, JsonRpcResponse>,
private val ethereumWsFactory: EthereumWsFactory? = null,
options: UpstreamsConfig.Options,
val node: QuorumForLabels.QuorumItem,
targets: CallMethods,
private val objectMapper: ObjectMapper
) : DefaultUpstream<EthereumApi>(id, options, targets), Upstream<EthereumApi>, 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<JsonRpcRequest, JsonRpcResponse>, 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<DirectEthereumApi> {
return Mono.just(api)
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
return directReader
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
@@ -121,18 +122,11 @@ open class EthereumUpstream(
}
@Suppress("unchecked")
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
}
return castApi(apiType) as T
}
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as Upstream<A>
return this as T
}
}

View File

@@ -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<UpstreamAvailability> {
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)
}

View File

@@ -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<BlockContainer>()
.name("new-blocks")
.build()
var basicAuth: AuthConfig.ClientBasicAuth? = null
private var blockCache: Reader<BlockId, BlockContainer> = 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<TransactionRefJson>) {
// 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<Any>(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<BlockContainer> {
return api
.executeAndConvert(Commands.eth().getBlock(io.infinitape.etherjar.domain.BlockHash(hash.value)))
.map { BlockContainer.from(it, objectMapper) }
}
fun getFlux(): Flux<BlockContainer> {
return Flux.from(this.topic)
.onBackpressureLatest()
}
override fun setCaches(caches: Caches) {
blockCache = caches.getBlocksByHash()
}
}

View File

@@ -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<BlockContainer>()
.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<TransactionRefJson>) {
// 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<Any>(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<BlockContainer> {
return Flux.from(this.topic)
.onBackpressureLatest()
}
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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 directApi: Reader<JsonRpcRequest, JsonRpcResponse>,
private val methods: CallMethods
) : Reader<JsonRpcRequest, JsonRpcResponse> {
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<JsonRpcResponse> {
if (!methods.isAllowed(key.method)) {
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
}
if (methods.isHardcoded(key.method)) {
return Mono.just(methods.executeHardcoded(key.method))
.map { JsonRpcResponse(it, null) }
}
val common = commonRequests(key)
if (common != null) {
return common.map { JsonRpcResponse(it, null) }
}
return directApi.read(key)
}
/**
* 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<ByteArray>? {
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
}
}
}

View File

@@ -27,13 +27,15 @@ 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.*
@@ -58,16 +60,15 @@ open class EthereumGrpcUpstream(
private val chain: Chain,
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val objectMapper: ObjectMapper,
private val rpcClient: ReactorEmeraldClient
) : DefaultUpstream<EthereumApi>(
private val client: JsonRpcGrpcClient
) : DefaultUpstream(
"$parentId/${chain.chainCode}",
UpstreamsConfig.Options.getDefaults(),
null
), CachesEnabled, Lifecycle {
), Lifecycle {
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
private var caches: Caches? = null
private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels())
private val head = DefaultEthereumHead()
@@ -76,16 +77,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<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
override fun start() {
if (this.isRunning) return
@@ -123,6 +115,7 @@ open class EthereumGrpcUpstream(
BigInteger(1, value.weight.toByteArray()),
Instant.ofEpochMilli(value.timestamp),
false,
null,
null
)
block
@@ -132,9 +125,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 +203,16 @@ open class EthereumGrpcUpstream(
return head
}
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> {
return Mono.just(createApi(matcher))
}
override fun setCaches(caches: Caches) {
this.caches = caches
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
return defaultReader
}
@SuppressWarnings("unchecked")
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
override fun <T : Upstream> cast(selfType: Class<T>): 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 <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as Upstream<A>
}
}

View File

@@ -22,9 +22,11 @@ import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.cache.CachesFactory
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
@@ -48,7 +50,8 @@ class GrpcUpstreams(
private val port: Int,
private val objectMapper: ObjectMapper,
private val auth: AuthConfig.ClientTlsAuth? = null,
private val fileResolver: FileResolver
private val fileResolver: FileResolver,
private val cachesFactory: CachesFactory
) {
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
@@ -57,7 +60,6 @@ class GrpcUpstreams(
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
private val known = HashMap<Chain, EthereumGrpcUpstream>()
private val lock = ReentrantLock()
private var grpcTransport: ReactorEmeraldClient? = null
fun start(): Flux<UpstreamChange> {
val channel: ManagedChannelBuilder<*> = if (auth != null && StringUtils.isNotEmpty(auth.ca)) {
@@ -74,10 +76,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<Disposable>()
@@ -162,7 +160,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()

View File

@@ -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<JsonRpcRequest, JsonRpcResponse> {
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<JsonRpcRequest, JsonRpcResponse> {
private val parser = JsonRpcParser()
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
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))
}
}
}
}
}

View File

@@ -17,26 +17,34 @@ 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
/**
* JSON RPC client
*/
class JsonRpcClient(
class JsonRpcHttpClient(
private val target: String,
private val objectMapper: ObjectMapper,
basicAuth: AuthConfig.ClientBasicAuth?
) {
basicAuth: AuthConfig.ClientBasicAuth? = null,
tlsCAAuth: ByteArray? = null
) : Reader<JsonRpcRequest, JsonRpcResponse> {
companion object {
private val log = LoggerFactory.getLogger(JsonRpcClient::class.java)
private val log = LoggerFactory.getLogger(JsonRpcHttpClient::class.java)
}
private val parser = JsonRpcParser()
@@ -49,22 +57,28 @@ class JsonRpcClient(
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)
}
this.httpClient = build
}
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()
fun execute(request: JsonRpcRequest): Mono<JsonRpcResponse> {
return Mono.just(request)
.map { it.toJson(objectMapper) }
.flatMap(this@JsonRpcClient::execute)
.map(parser::parse)
build.secure { spec ->
spec.sslContext(sslContext)
}
}
this.httpClient = build
}
fun execute(request: ByteArray): Mono<ByteArray> {
@@ -77,4 +91,11 @@ class JsonRpcClient(
.aggregate()
.asByteArray()
}
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
return Mono.just(key)
.map { it.toJson(objectMapper) }
.flatMap(this@JsonRpcHttpClient::execute)
.map(parser::parse)
}
}

View File

@@ -18,12 +18,62 @@ 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(
val result: ByteArray?,
private val result: ByteArray?,
val error: ResponseError?
) {
companion object {
private val NULL_VALUE = "null".toByteArray()
}
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<ByteArray> {
return if (error != null) {
Mono.error(error.asException())
} else {
Mono.just(getResult())
}
}
fun requireStringResult(): Mono<String> {
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
@@ -43,7 +93,11 @@ class JsonRpcResponse(
return result1
}
class ResponseError(val code: Int, val message: String)
class ResponseError(val code: Int, val message: String) {
fun asException(): RpcException {
return RpcException(code, message)
}
}
class ResponseJsonSerializer : JsonSerializer<JsonRpcResponse>() {
override fun serialize(value: JsonRpcResponse, gen: JsonGenerator, serializers: SerializerProvider) {

View File

@@ -63,6 +63,7 @@ class BlocksRedisCacheSpec extends Specification {
Instant.ofEpochSecond(10501050),
false,
"test".bytes,
null,
[TxId.from(hash2), TxId.from(hash1)]
)

View File

@@ -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)

View File

@@ -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"
}
}

View File

@@ -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,28 @@ 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"
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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]
}
}
}

View File

@@ -21,8 +21,6 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
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.Selector
import io.emeraldpay.dshackle.upstream.Upstream
@@ -31,9 +29,9 @@ 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
@@ -47,9 +45,7 @@ class NativeCallSpec extends Specification {
setup:
def quorum = Spy(new AlwaysQuorum())
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
def apiMock = TestingCommons.api()
apiMock.answer("eth_test", [], "foo")
@@ -60,21 +56,19 @@ class NativeCallSpec extends Specification {
when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def act = objectMapper.readValue(resp.payload, Map)
def act = objectMapper.readValue(resp.payload, Object)
then:
act == [jsonrpc:"2.0", id:1, result: "foo"]
act == "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 quorum = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_test", [], null)
apiMock.answerOnce("eth_test", [], "bar")
@@ -88,21 +82,19 @@ class NativeCallSpec extends Specification {
when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def act = objectMapper.readValue(resp.payload, Map)
def act = objectMapper.readValue(resp.payload, Object)
then:
act == [jsonrpc:"2.0", id:1, result: "bar"]
act == "bar"
2 * quorum.record(_, _)
1 * quorum.getResult()
}
def "Have pause between repeats"() {
setup:
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
def quorum = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_test", [], null)
apiMock.answerOnce("eth_test", [], "bar")
@@ -117,19 +109,19 @@ class NativeCallSpec extends Specification {
def t1 = System.currentTimeMillis()
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def delta = System.currentTimeMillis() - t1
def act = objectMapper.readValue(resp.payload, Object)
then:
delta > 95 // should be 100, but sometimes gives less ???
new String(resp.payload) == '{"jsonrpc":"2.0","id":1,"result":"bar"}'
act == "bar"
}
def "One call has no pause"() {
setup:
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
def quorum = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_test", [], "bar")
@@ -149,12 +141,11 @@ class NativeCallSpec extends Specification {
def "Returns error if no quorum"() {
setup:
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
def quorum = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
def apiMock = TestingCommons.api()
apiMock.answer("eth_test", [], null, 3)
apiMock.answerOnce("eth_test", [], "foo")
@@ -215,7 +206,7 @@ class NativeCallSpec extends Specification {
when:
def resp = nativeCall.buildResponse(
new NativeCall.CallContext<byte[]>(1561, TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi)), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json))
new NativeCall.CallContext<byte[]>(1561, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json))
)
then:
resp.id == 1561
@@ -270,14 +261,14 @@ class NativeCallSpec extends Specification {
.verify(Duration.ofSeconds(1))
}
@Ignore
//TODO
def "Calls cache before remote"() {
setup:
def upstreams = Stub(Upstreams)
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<NativeCall.ParsedCallDetails>(10,
upstream,
@@ -289,13 +280,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 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<NativeCall.ParsedCallDetails>(10,
upstream,
@@ -313,9 +304,7 @@ class NativeCallSpec extends Specification {
def quorum = Spy(new AlwaysQuorum())
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
def apiMock = TestingCommons.api()
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 1"))
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 2"))
@@ -329,21 +318,19 @@ class NativeCallSpec extends Specification {
when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def act = objectMapper.readValue(resp.payload, Map)
def act = objectMapper.readValue(resp.payload, Object)
then:
act == [jsonrpc:"2.0", id:1, result: "bar"]
act == "bar"
1 * quorum.record(_, _)
1 * quorum.getResult()
}
def "Send raw retries 3 times"() {
setup:
def quorum = Spy(new BroadcastQuorum(TestingCommons.rpcConverter(), 3))
def quorum = Spy(new BroadcastQuorum(TestingCommons.objectMapper(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
def apiMock = TestingCommons.api()
apiMock.answer("eth_sendRawTransaction", ["0x1234"],
"0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b", 1)
@@ -360,9 +347,9 @@ class NativeCallSpec extends Specification {
when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def act = objectMapper.readValue(resp.payload, Map)
def act = objectMapper.readValue(resp.payload, Object)
then:
act == [jsonrpc:"2.0", id:1, result: "0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b"]
act == "0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b"
1 * quorum.record(_ as byte[], _)
2 * quorum.record(_ as RpcException, _)
}

View File

@@ -24,12 +24,9 @@ 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.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
@@ -80,7 +77,7 @@ class StreamHeadSpec extends Specification {
.build()
}
def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, Stub(DirectEthereumApi.class))
def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, TestingCommons.api())
def upstreams = new UpstreamsMock(Chain.ETHEREUM, upstream)
def streamHead = new StreamHead(upstreams)
when:

View File

@@ -19,12 +19,18 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.test.ReaderMock
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.test.UpstreamsMock
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
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.bitcoin.BitcoinChainUpstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@@ -216,24 +222,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(BitcoinChainUpstreams) {
_ * 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
}
}
Upstreams upstreams = new UpstreamsMock(Chain.BITCOIN, upstream)
TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams)
when:
@@ -253,7 +259,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 {

View File

@@ -17,11 +17,12 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams
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
@@ -45,8 +46,8 @@ class TrackBitcoinTxSpec extends Specification {
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
])
}
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
_ * getData() >> Mock(BitcoinReader) {
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
_ * getReader() >> Mock(BitcoinReader) {
_ * getMempool() >> mempoolAccess
}
}
@@ -71,8 +72,8 @@ class TrackBitcoinTxSpec extends Specification {
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
])
}
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
_ * getData() >> Mock(BitcoinReader) {
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
_ * getReader() >> Mock(BitcoinReader) {
_ * getMempool() >> mempoolAccess
}
}
@@ -92,13 +93,15 @@ class TrackBitcoinTxSpec extends Specification {
setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
1 * getTx(txid) >> Mono.just([
txid: txid
])
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
_ * 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)
@@ -113,15 +116,17 @@ class TrackBitcoinTxSpec extends Specification {
setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
1 * getTx(txid) >> Mono.just([
txid : txid,
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
height : 100
])
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
_ * 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)
@@ -140,12 +145,12 @@ class TrackBitcoinTxSpec extends Specification {
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) {
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
1 * getHead() >> head
}
def status = new TrackBitcoinTx.TxStatus(
@@ -171,12 +176,12 @@ class TrackBitcoinTxSpec extends Specification {
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 +196,9 @@ class TrackBitcoinTxSpec extends Specification {
])
]
}
Upstream upstream = Mock(BitcoinUpstream) {
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
1 * getHead() >> head
_ * getApi(_) >> Mono.just(api)
_ * getReader() >> api
}
def status = new TrackBitcoinTx.TxStatus(
txid, false, null, false, null, null, null, 0
@@ -212,11 +217,7 @@ class TrackBitcoinTxSpec extends Specification {
setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
1 * getTx(txid) >> Mono.just([
txid: txid
])
}
Head head = Mock(Head) {
_ * getFlux() >> Flux.empty()
}
@@ -228,17 +229,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
}
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
_ * 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:
@@ -254,7 +258,7 @@ class TrackBitcoinTxSpec extends Specification {
setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
BitcoinReader api = Mock(BitcoinReader) {
_ * getTx(txid) >> Mono.just([
txid : txid,
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
@@ -267,18 +271,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)
BitcoinChainUpstreams upstream = Mock(BitcoinChainUpstreams) {
_ * 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)

View File

@@ -64,7 +64,7 @@ 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)
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
@@ -104,7 +104,7 @@ 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)
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)

View File

@@ -26,12 +26,10 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.test.UpstreamsMock
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.ethereum.EthereumChainUpstream
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,7 +96,7 @@ 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)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
@@ -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) {
((EthereumChainUpstream) 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(EthereumChainUpstream)) },
{ scheduler },
5)
@@ -168,7 +166,7 @@ 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)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
@@ -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)
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)
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,7 +286,7 @@ 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)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)

View File

@@ -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<JsonRpcRequest, JsonRpcResponse> {
private static final Logger log = LoggerFactory.getLogger(this)
List<PredefinedResponse> 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<byte[]> execute(int id, @NotNull String method, @NotNull List<?> params) {
Callable<byte[]> call = {
def predefined = predefined.find { it.isSame(id, method, params) }
ResponseJson json = new ResponseJson<Object, Integer>(id: id)
Mono<JsonRpcResponse> read(JsonRpcRequest request) {
Callable<JsonRpcResponse> 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<Object, Integer>(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<byte[]>
return new JsonRpcResponse(result, error)
} as Callable<JsonRpcResponse>
return Mono.fromCallable(call)
}
def nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> 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

View File

@@ -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<JsonRpcRequest, JsonRpcResponse> {
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<RpcCallResponse> execute(ReactorBatch batch) {
return Flux.error(new Exception("Not implemented in mock"))
}
@Override
def <JS, RES> Mono<RES> execute(RpcCall<JS, RES> call) {
return Mono.error(new Exception("Not implemented in mock"))
}
@Override
Mono<JsonRpcResponse> read(JsonRpcRequest key) {
return Mono.error(new Exception("Not implemented in mock"))
}
}

View File

@@ -16,18 +16,19 @@
*/
package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.startup.QuorumForLabels
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.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,19 +36,19 @@ class EthereumUpstreamMock extends EthereumUpstream {
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) {
EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
}
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) {
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(id, chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
}
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) {
EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> 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<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
super(id, chain, api, null,
UpstreamsConfig.Options.getDefaults(), new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
methods, TestingCommons.objectMapper())

View File

@@ -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<K, D> implements Reader<K, D> {
/**
* @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<Any>): Mono<ByteArray>
private Map<K, D> mapping = new HashMap<K, D>()
}
ReaderMock() {
}
ReaderMock with(K key, D data) {
mapping[key] = data
return this
}
@Override
Mono<D> read(K key) {
return Mono.justOrEmpty(mapping.get(key))
}
}

View File

@@ -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.reader.Reader
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
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.EthereumChainUpstream
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,32 @@ 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<JsonRpcRequest, JsonRpcResponse> api) {
return new EthereumUpstreamMock(Chain.ETHEREUM, api)
}
static EthereumUpstreamMock upstream(DirectEthereumApi api, String method) {
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, String method) {
return upstream(api, [method])
}
static EthereumUpstreamMock upstream(DirectEthereumApi api, List<String> methods) {
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, List<String> methods) {
return new EthereumUpstreamMock(Chain.ETHEREUM, api, new DirectCallMethods(methods))
}
static AggregatedUpstream aggregatedUpstream(DirectEthereumApi api) {
static AggregatedUpstream aggregatedUpstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
return aggregatedUpstream(upstream(api))
}
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) {
return new AggregatedEthereumUpstreams(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper())
return new EthereumChainUpstream(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper())
}
static CachesFactory emptyCaches() {

View File

@@ -17,12 +17,15 @@
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.bitcoin.BitcoinChainUpstreams
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.ethereum.EthereumChainUpstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain
@@ -32,30 +35,39 @@ import reactor.core.publisher.Flux
class UpstreamsMock implements Upstreams {
private Map<Chain, DefaultEthereumMethods> target = [:]
private Map<Chain, AggregatedEthereumUpstreamsMock> upstreams = [:]
private Map<Chain, AggregatedUpstream> upstreams = [:]
UpstreamsMock(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) {
AggregatedUpstream 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 EthereumChainUpstream) {
upstreams[chain] = up
} else if (up instanceof EthereumUpstream) {
upstreams[chain] = new EthereumChainUpstreamMock(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 BitcoinChainUpstreams) {
upstreams[chain] = up
} else if (up instanceof BitcoinUpstream) {
upstreams[chain] = new BitcoinChainUpstreams(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) {
return upstreams[chain]
@@ -85,12 +97,12 @@ class UpstreamsMock implements Upstreams {
return upstreams.containsKey(chain)
}
static class AggregatedEthereumUpstreamsMock extends AggregatedEthereumUpstreams {
static class EthereumChainUpstreamMock extends EthereumChainUpstream {
EthereumReader customReader = null
AggregatedEthereumUpstreamsMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches, @NotNull ObjectMapper objectMapper) {
super(chain, upstreams, caches, objectMapper)
EthereumChainUpstreamMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches) {
super(chain, upstreams, caches, TestingCommons.objectMapper())
}
@Override

View File

@@ -21,8 +21,7 @@ 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.EthereumChainUpstream
import io.emeraldpay.grpc.Chain
import spock.lang.Specification
@@ -30,9 +29,9 @@ class AggregatedUpstreamSpec 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 EthereumChainUpstream(Chain.ETHEREUM, [up1, up2], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper())
when:
aggr.onUpstreamsUpdated()
def act = aggr.getMethods()

View File

@@ -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<TransactionRefJson>(
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<TransactionRefJson>(
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<TransactionRefJson>(
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<TransactionRefJson>(
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<TransactionRefJson>(
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<TransactionRefJson>(
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<TransactionRefJson>(
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()
}
}

View File

@@ -27,7 +27,7 @@ class CurrentUpstreamsSpec 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 up = new EthereumUpstreamMock("test", Chain.ETHEREUM, TestingCommons.api())
when:
current.update(new UpstreamChange(Chain.ETHEREUM, up, UpstreamChange.ChangeType.ADDED))
then:
@@ -38,9 +38,9 @@ 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 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))
@@ -54,10 +54,10 @@ 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 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))
@@ -72,7 +72,7 @@ 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 up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, TestingCommons.api())
when:
def act = current.isAvailable(Chain.ETHEREUM)

View File

@@ -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<EthereumUpstream> 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()
@@ -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,8 +154,8 @@ 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 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")

View File

@@ -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))

View File

@@ -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}')
)
}
}

View File

@@ -124,6 +124,6 @@ class AggregatedCallMethodsSpec extends Specification {
when:
def act = aggregate.executeHardcoded("eth_test")
then:
act == "hello"
new String(act) == "hello"
}
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -17,17 +17,26 @@ package io.emeraldpay.dshackle.upstream.ethereum
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.TxContainer
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.test.EthereumUpstreamMock
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.test.UpstreamsMock
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
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.RpcException
import io.infinitape.etherjar.rpc.RpcResponseError
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
@@ -35,6 +44,7 @@ import reactor.core.publisher.Mono
import spock.lang.Specification
import java.time.Instant
import java.time.temporal.ChronoUnit
class EthereumReaderSpec extends Specification {
@@ -63,7 +73,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(AggregatedUpstream), caches, TestingCommons.objectMapper())
when:
def act = reader.blocksById().read(blockId).block()
@@ -81,8 +91,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 +113,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 +135,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(AggregatedUpstream), caches, TestingCommons.objectMapper())
when:
def act = reader.blocksByHash().read(blockJson.hash).block()
@@ -145,8 +153,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 +174,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(AggregatedUpstream), caches, TestingCommons.objectMapper())
when:
def act = reader.txByHash().read(txJson.hash).block()
@@ -186,8 +193,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 +207,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:

View File

@@ -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<TransactionRefJson>()
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<TransactionRefJson>()
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()

View File

@@ -25,6 +25,7 @@ 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
@@ -46,7 +47,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 +74,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 +90,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 +131,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 +150,7 @@ class EthereumGrpcUpstreamSpec extends Specification {
def callData = [:]
def finished = new CompletableFuture<Boolean>()
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 +192,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"])

View File

@@ -1,59 +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.rpcclient
import io.emeraldpay.dshackle.test.TestingCommons
import org.mockserver.integration.ClientAndServer
import org.mockserver.model.HttpRequest
import org.mockserver.model.HttpResponse
import spock.lang.Specification
class JsonRpcClientSpec extends Specification {
ClientAndServer mockServer
JsonRpcClient client
def setup() {
mockServer = ClientAndServer.startClientAndServer(18332);
client = new JsonRpcClient("localhost:18332", TestingCommons.objectMapper(), null)
}
def cleanup() {
mockServer.stop()
}
def "Make a request"() {
setup:
def resp = '{' +
' "jsonrpc": "2.0",' +
' "result": "0x98de45",' +
' "error": null,' +
' "id": 15' +
'}'
mockServer.when(
HttpRequest.request()
).respond(
HttpResponse.response(resp)
)
when:
def act = client.execute(new JsonRpcRequest("test", [])).block()
then:
act.error == null
new String(act.result) == '"0x98de45"'
}
}

View File

@@ -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()

Some files were not shown because too many files have changed in this diff Show More