Merge pull request #6 from emeraldpay/feat/normal-request-flow

refactor request flow
This commit is contained in:
Igor Artamonov
2020-05-16 23:07:06 -04:00
committed by GitHub
117 changed files with 3334 additions and 2706 deletions

View File

@@ -81,7 +81,6 @@ dependencies {
implementation "io.infinitape:etherjar-hex:$etherjarVersion" implementation "io.infinitape:etherjar-hex:$etherjarVersion"
implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion" implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion"
implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion" implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion"
implementation "io.infinitape:etherjar-rpc-emerald:$etherjarVersion"
implementation "io.infinitape:etherjar-tx:$etherjarVersion" implementation "io.infinitape:etherjar-tx:$etherjarVersion"
implementation 'org.bitcoinj:bitcoinj-core:0.15.8' implementation 'org.bitcoinj:bitcoinj-core:0.15.8'
@@ -125,7 +124,7 @@ compileTestKotlin {
test { test {
jvmArgs '-ea' jvmArgs '-ea'
testLogging.showStandardStreams = true testLogging.showStandardStreams = false
testLogging.exceptionFormat = 'full' testLogging.exceptionFormat = 'full'
} }
@@ -199,4 +198,38 @@ task generateVersion() {
].join("\n") ].join("\n")
} }
}
// Show the list of failed tests and output only for them, helpful for CI
ext.failedTests = []
tasks.withType(Test) {
def stdout = new LinkedList<String>()
beforeTest { TestDescriptor td ->
stdout.clear()
}
onOutput { TestDescriptor td, TestOutputEvent toe ->
stdout.addAll(toe.getMessage().split('(?m)$'))
while (stdout.size() > 100) {
stdout.remove()
}
}
afterTest { TestDescriptor descriptor, TestResult result ->
if(result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.FAILURE){
failedTests << "${descriptor.className} > ${descriptor.name}"
if (!stdout.isEmpty()) {
println("-------- ${descriptor.className} > ${descriptor.name} OUTPUT ".padRight(120, "-"))
stdout.each { print(it) }
println("================".padRight(120, "="))
}
}
}
}
gradle.buildFinished {
if(!failedTests.empty){
println "Failed tests for ${project.name}:"
failedTests.each { failedTest ->
println failedTest
}
println ""
}
} }

View File

@@ -93,6 +93,7 @@ class BlocksRedisCache(
Instant.ofEpochMilli(meta.timestamp), Instant.ofEpochMilli(meta.timestamp),
false, false,
value.value.toByteArray(), value.value.toByteArray(),
null,
meta.txHashesList.map { meta.txHashesList.map {
TxId(it.toByteArray()) 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.data.TxId
import io.emeraldpay.dshackle.reader.CompoundReader import io.emeraldpay.dshackle.reader.CompoundReader
import io.emeraldpay.dshackle.reader.Reader 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.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -158,11 +159,11 @@ open class Caches(
} }
fun getFullBlocks(): Reader<BlockId, BlockContainer> { fun getFullBlocks(): Reader<BlockId, BlockContainer> {
return EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash) return EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash)
} }
fun getFullBlocksByHeight(): Reader<Long, BlockContainer> { fun getFullBlocksByHeight(): Reader<Long, BlockContainer> {
return BlockByHeight(blocksByHeight, EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash)) return BlockByHeight(blocksByHeight, EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash))
} }
enum class Tag { enum class Tag {

View File

@@ -59,7 +59,10 @@ class TxRedisCache(
fun toProto(value: TxContainer): ByteArray { fun toProto(value: TxContainer): ByteArray {
val meta = CachesProto.TxMeta.newBuilder() val meta = CachesProto.TxMeta.newBuilder()
.setHash(ByteString.copyFrom(value.hash.value)) .setHash(ByteString.copyFrom(value.hash.value))
.setHeight(value.height)
value.height?.let {
meta.setHeight(it)
}
value.blockId?.value?.let { value.blockId?.value?.let {
meta.setBlockHash(ByteString.copyFrom(it)) meta.setBlockHash(ByteString.copyFrom(it))

View File

@@ -86,19 +86,21 @@ class UpstreamsConfig {
open class UpstreamConnection open class UpstreamConnection
open class RpcConnection : UpstreamConnection() {
var rpc: HttpEndpoint? = null
}
class GrpcConnection : UpstreamConnection() { class GrpcConnection : UpstreamConnection() {
var host: String? = null var host: String? = null
var port: Int = 0 var port: Int = 0
var auth: AuthConfig.ClientTlsAuth? = null var auth: AuthConfig.ClientTlsAuth? = null
} }
class EthereumConnection : UpstreamConnection() { class EthereumConnection : RpcConnection() {
var rpc: HttpEndpoint? = null
var ws: WsEndpoint? = null var ws: WsEndpoint? = null
} }
class BitcoinConnection : UpstreamConnection() { class BitcoinConnection : RpcConnection() {
var rpc: HttpEndpoint? = null
} }
class HttpEndpoint(val url: URI) { 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.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.apache.commons.codec.binary.Hex
import java.math.BigInteger import java.math.BigInteger
import java.time.Instant import java.time.Instant
@@ -30,12 +31,13 @@ class BlockContainer(
val timestamp: Instant, val timestamp: Instant,
val full: Boolean, val full: Boolean,
json: ByteArray?, json: ByteArray?,
val parsed: Any?,
val transactions: List<TxId> = emptyList() val transactions: List<TxId> = emptyList()
) : SourceContainer(json) { ) : SourceContainer(json, parsed) {
companion object { companion object {
@JvmStatic @JvmStatic
fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer { fun from(block: BlockJson<*>, raw: ByteArray): BlockContainer {
val hasTransactions = block.transactions?.filterIsInstance<TransactionJson>()?.count() ?: 0 > 0 val hasTransactions = block.transactions?.filterIsInstance<TransactionJson>()?.count() ?: 0 > 0
return BlockContainer( return BlockContainer(
block.number, block.number,
@@ -43,10 +45,26 @@ class BlockContainer(
block.totalDifficulty, block.totalDifficulty,
block.timestamp, block.timestamp,
hasTransactions, hasTransactions,
objectMapper.writeValueAsBytes(block), raw,
block,
block.transactions?.map { TxId.from(it.hash) } ?: emptyList() block.transactions?.map { TxId.from(it.hash) } ?: emptyList()
) )
} }
@JvmStatic
fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer {
return from(block, objectMapper.writeValueAsBytes(block))
}
@JvmStatic
fun from(raw: ByteArray, objectMapper: ObjectMapper): BlockContainer {
val block = objectMapper.readValue(raw, BlockJson::class.java)
return from(block, raw)
}
}
override fun toString(): String {
return "Block $height = $hash"
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -25,6 +25,11 @@ class BlockId(
) : HashId(value) { ) : HashId(value) {
companion object { companion object {
@JvmStatic
fun from(hash: ByteArray): BlockId {
return BlockId(hash)
}
@JvmStatic @JvmStatic
fun from(hash: BlockHash): BlockId { fun from(hash: BlockHash): BlockId {
return BlockId(hash.bytes) return BlockId(hash.bytes)

View File

@@ -40,6 +40,10 @@ open class HashId(
return String(hex) return String(hex)
} }
fun toHexWithPrefix(): String {
return "0x" + toHex()
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is HashId) return false if (other !is HashId) return false

View File

@@ -16,10 +16,24 @@
*/ */
package io.emeraldpay.dshackle.data package io.emeraldpay.dshackle.data
import java.lang.ClassCastException
abstract class SourceContainer( 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is SourceContainer) return false 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 import io.infinitape.etherjar.rpc.json.TransactionJson
class TxContainer( class TxContainer(
val height: Long, val height: Long?,
val hash: TxId, val hash: TxId,
val blockId: BlockId?, val blockId: BlockId?,
json: ByteArray? json: ByteArray?,
) : SourceContainer(json) { parsed: Any? = null
) : SourceContainer(json, parsed) {
companion object { companion object {
@JvmStatic @JvmStatic
fun from(tx: TransactionJson, objectMapper: ObjectMapper): TxContainer { fun from(tx: TransactionJson, objectMapper: ObjectMapper): TxContainer {
return from(tx, objectMapper.writeValueAsBytes(tx))
}
fun from(tx: TransactionJson, raw: ByteArray): TxContainer {
return TxContainer( return TxContainer(
tx.blockNumber, tx.blockNumber,
TxId.from(tx.hash), TxId.from(tx.hash),
BlockId.from(tx.blockHash), tx.blockHash?.let { BlockId.from(it) },
objectMapper.writeValueAsBytes(tx) raw,
tx
) )
} }
} }

View File

@@ -32,13 +32,17 @@ open class AlwaysQuorum: CallQuorum {
return resolved return resolved
} }
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean { override fun isFailed(): Boolean {
return false
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
result = response result = response
resolved = true resolved = true
return true return true
} }
override fun record(error: RpcException, upstream: Upstream<*>) { override fun record(error: RpcException, upstream: Upstream) {
} }
override fun getResult(): ByteArray? { override fun getResult(): ByteArray? {

View File

@@ -16,14 +16,15 @@
*/ */
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.JacksonRpcConverter
open class BroadcastQuorum( open class BroadcastQuorum(
jacksonRpcConverter: JacksonRpcConverter, objectMapper: ObjectMapper,
val quorum: Int = 3 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 result: ByteArray? = null
private var txid: String? = null private var txid: String? = null
@@ -36,11 +37,15 @@ open class BroadcastQuorum(
return calls >= quorum return calls >= quorum
} }
override fun isFailed(): Boolean {
return false
}
override fun getResult(): ByteArray? { override fun getResult(): ByteArray? {
return result return result
} }
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) { override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
calls++ calls++
if (txid == null && responseValue != null) { if (txid == null && responseValue != null) {
txid = responseValue txid = responseValue
@@ -48,7 +53,7 @@ open class BroadcastQuorum(
} }
} }
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) { override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low" // can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
calls++ calls++
if (result == null) { if (result == null) {

View File

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

View File

@@ -16,15 +16,16 @@
*/ */
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.JacksonRpcConverter
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
open class NonEmptyQuorum( open class NonEmptyQuorum(
jacksonRpcConverter: JacksonRpcConverter, objectMapper: ObjectMapper,
val maxTries: Int = 3 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 result: ByteArray? = null
private var tries: Int = 0 private var tries: Int = 0
@@ -33,10 +34,14 @@ open class NonEmptyQuorum(
} }
override fun isResolved(): Boolean { override fun isResolved(): Boolean {
return result != null || tries >= maxTries return result != null
} }
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*>) { override fun isFailed(): Boolean {
return tries >= maxTries
}
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
tries++ tries++
if (responseValue != null) { if (responseValue != null) {
result = response result = response
@@ -47,10 +52,12 @@ open class NonEmptyQuorum(
return result return result
} }
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) { override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
tries++
} }
override fun record(error: RpcException, upstream: Upstream<*>) { override fun record(error: RpcException, upstream: Upstream) {
tries++
} }
} }

View File

@@ -16,6 +16,7 @@
*/ */
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.hex.HexQuantity import io.infinitape.etherjar.hex.HexQuantity
@@ -25,9 +26,9 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
open class NonceQuorum( open class NonceQuorum(
jacksonRpcConverter: JacksonRpcConverter, objectMapper: ObjectMapper,
val tries: Int = 3 val tries: Int = 3
): CallQuorum, ValueAwareQuorum<String>(jacksonRpcConverter, String::class.java) { ) : CallQuorum, ValueAwareQuorum<String>(objectMapper, String::class.java) {
private val lock = ReentrantLock() private val lock = ReentrantLock()
private var resultValue = 0L private var resultValue = 0L
@@ -40,11 +41,15 @@ open class NonceQuorum(
override fun isResolved(): Boolean { override fun isResolved(): Boolean {
lock.withLock { lock.withLock {
return receivedTimes >= tries || errors >= tries return receivedTimes >= tries && !isFailed()
} }
} }
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) { override fun isFailed(): Boolean {
return errors >= tries
}
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
val value = responseValue?.let { str -> val value = responseValue?.let { str ->
HexQuantity.from(str).value.toLong() HexQuantity.from(str).value.toLong()
} }
@@ -63,11 +68,7 @@ open class NonceQuorum(
return result return result
} }
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) { override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
errors++
}
override fun record(error: RpcException, upstream: Upstream<*>) {
errors++ errors++
} }

View File

@@ -32,7 +32,11 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return result.get() != null return result.get() != null
} }
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean { override fun isFailed(): Boolean {
return false
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
val lagging = upstream.getLag() > maxLag val lagging = upstream.getLag() > maxLag
if (!lagging) { if (!lagging) {
result.set(response) result.set(response)
@@ -41,10 +45,9 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return false return false
} }
override fun record(error: RpcException, upstream: Upstream<*>) { override fun record(error: RpcException, upstream: Upstream) {
} }
override fun getResult(): ByteArray { override fun getResult(): ByteArray {
return result.get() return result.get()
} }

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
// creates instance of a Quorum based reader
interface QuorumReaderFactory {
companion object {
fun default(): QuorumReaderFactory {
return Default()
}
}
fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result>
class Default : QuorumReaderFactory {
override fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
return QuorumRpcReader(apis, quorum)
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.RpcException
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.util.function.Tuples
/**
* Makes request with applying Quorum
*/
class QuorumRpcReader(
private val apis: ApiSource,
private val quorum: CallQuorum
) : Reader<JsonRpcRequest, QuorumRpcReader.Result> {
companion object {
private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java)
}
override fun read(key: JsonRpcRequest): Mono<QuorumRpcReader.Result> {
apis.request(1)
// uses a mix of retry strategy and managed Publisher for calls.
// retry is used when an error happened
// but if no error received, we check quorum and if not enough data received we request more
// eventually source of upstreams is Completed (or something Errored) and if finalizes the result
val retrySpec = reactor.util.retry.Retry.from { signal ->
signal.takeUntil {
it.totalRetries() >= 3 || quorum.isResolved() || quorum.isFailed()
}.doOnNext {
// need one more API source if retried
apis.request(1)
}
}
return Flux.from(apis)
.flatMap { api ->
api.getApi().read(key)
.flatMap(JsonRpcResponse::requireResult)
// on error notify quorum, it may use error message or other details
.doOnError { err ->
if (err is RpcException) {
quorum.record(err, api)
}
}
.map { Tuples.of(it, api) }
}
.retryWhen(retrySpec)
// record all correct responses until quorum reached
.reduce(quorum, { res, a ->
if (res.record(a.t1, a.t2)) {
apis.resolve()
} else {
apis.request(1)
}
res
})
// if last call resulted in error it's still possible that request was resolved correctly. i.e. for BroadcastQuorum
.onErrorResume { err ->
if (quorum.isResolved()) {
Mono.just(quorum)
} else {
Mono.error(err)
}
}
.doOnNext {
if (!it.isResolved()) {
log.debug("No quorum for ${key.method} as ${quorum}")
}
}
// return nothing if not resolved
.filter { it.isResolved() }
.map {
// TODO find actual quorum number
QuorumRpcReader.Result(it.getResult()!!, 1)
}
}
class Result(
val value: ByteArray,
val quorum: Int
)
}

View File

@@ -16,23 +16,24 @@
*/ */
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.JacksonRpcConverter
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
abstract class ValueAwareQuorum<T>( abstract class ValueAwareQuorum<T>(
val jacksonRpcConverter: JacksonRpcConverter, val objectMapper: ObjectMapper,
val clazz: Class<T> val clazz: Class<T>
): CallQuorum { ): CallQuorum {
private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java) private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java)
fun extractValue(response: ByteArray, clazz: Class<T>): T? { 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 { try {
val value = extractValue(response, clazz) val value = extractValue(response, clazz)
recordValue(response, value, upstream) recordValue(response, value, upstream)
@@ -44,12 +45,12 @@ abstract class ValueAwareQuorum<T>(
return isResolved(); return isResolved();
} }
override fun record(error: RpcException, upstream: Upstream<*>) { override fun record(error: RpcException, upstream: Upstream) {
recordError(null, error.rpcMessage, 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 import reactor.core.publisher.Mono
interface Reader<K, D> { interface Reader<in K, D> {
fun read(key: K): Mono<D> fun read(key: K): Mono<D>

View File

@@ -29,15 +29,15 @@ import reactor.core.publisher.Mono
@Service @Service
class Describe( class Describe(
@Autowired private val upstreams: Upstreams, @Autowired private val multistreamHolder: MultistreamHolder,
@Autowired private val subscribeStatus: SubscribeStatus @Autowired private val subscribeStatus: SubscribeStatus
) { ) {
fun describe(requestMono: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> { fun describe(requestMono: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
return requestMono.map { _ -> return requestMono.map { _ ->
val resp = BlockchainOuterClass.DescribeResponse.newBuilder() val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
upstreams.getAvailable().forEach { chain -> multistreamHolder.getAvailable().forEach { chain ->
upstreams.getUpstream(chain)?.let { chainUpstreams -> multistreamHolder.getUpstream(chain)?.let { chainUpstreams ->
val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll()) val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll())
val targets = chainUpstreams.getMethods().getSupportedMethods() val targets = chainUpstreams.getMethods().getSupportedMethods()
val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder() val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder()

View File

@@ -24,8 +24,14 @@ import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
import io.emeraldpay.dshackle.quorum.QuorumRpcReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.RpcResponseError
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -36,21 +42,23 @@ import java.lang.Exception
@Service @Service
open class NativeCall( open class NativeCall(
@Autowired private val upstreams: Upstreams, @Autowired private val multistreamHolder: MultistreamHolder,
@Autowired private val objectMapper: ObjectMapper @Autowired private val objectMapper: ObjectMapper
) { ) {
private val log = LoggerFactory.getLogger(NativeCall::class.java) private val log = LoggerFactory.getLogger(NativeCall::class.java)
var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default()
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> { open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
return requestMono.flatMapMany(this::prepareCall) return requestMono.flatMapMany(this::prepareCall)
.map(this::setupCallParams) .map(this::setupCallParams)
.parallel() .parallel()
.flatMap(this::fetch) .flatMap(this::fetch)
.sequential() .sequential()
.map(this::buildResponse) .map(this::buildResponse)
.doOnError { e -> log.warn("Error during native call: ${e.message}") } .doOnError { e -> log.warn("Error during native call: ${e.message}") }
.onErrorResume(this::processException) .onErrorResume(this::processException)
} }
fun setupCallParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> { fun setupCallParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> {
@@ -87,17 +95,17 @@ open class NativeCall(
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number))) return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
} }
if (!upstreams.isAvailable(chain)) { if (!multistreamHolder.isAvailable(chain)) {
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number))) return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
} }
val upstream = upstreams.getUpstream(chain) val upstream = multistreamHolder.getUpstream(chain)
?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain))) ?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain)))
return prepareCall(request, upstream) return prepareCall(request, upstream)
} }
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<*>): Flux<CallContext<RawCallDetails>> { fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: Multistream): Flux<CallContext<RawCallDetails>> {
return request.itemsList.toFlux().map { return request.itemsList.toFlux().map {
val method = it.method val method = it.method
val params = it.payload.toStringUtf8() val params = it.payload.toStringUtf8()
@@ -115,75 +123,29 @@ open class NativeCall(
} }
fun fetch(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> { fun fetch(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
return fetchFromCache(ctx) return ctx.upstream.getRoutedApi(ctx.matcher)
.onErrorResume { t -> .flatMap { api ->
log.warn("Failed to read from cache", t); api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
Mono.empty() .flatMap(JsonRpcResponse::requireResult)
} .map {
.switchIfEmpty( ctx.withPayload(it)
}
}.switchIfEmpty(
Mono.just(ctx).flatMap(this::executeOnRemote) Mono.just(ctx).flatMap(this::executeOnRemote)
) )
} .onErrorMap {
CallFailure(ctx.id, it)
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) }
} }
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> { fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
val apis = ctx.getApis() if (!ctx.upstream.getMethods().isAllowed(ctx.payload.method)) {
apis.request(1) return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
var failures = 0 }
return Flux.from(apis) val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum)
.flatMap { api -> return reader.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
val upstream = ctx.upstream
api.execute(ctx.id, ctx.payload.method, ctx.payload.params)
// on error notify quorum, it may use error message or other details
.doOnError { err ->
if (err is RpcException) {
ctx.callQuorum.record(err, upstream)
}
}
.map { Tuples.of(it, upstream) }
}
.retry {
failures++
if (ctx.callQuorum.isResolved()) {
false
} else if (failures < 3) {
apis.request(1)
true
} else {
false
}
}
// record all correct responses until quorum reached
.reduce(ctx.callQuorum, {res, a ->
if (res.record(a.t1, a.t2)) {
apis.resolve()
} else {
apis.request(1)
}
res
})
// if last call resulted in error it's still possible that request was resolved correctly. i.e. for BroadcastQuorum
.onErrorResume { err ->
if (ctx.callQuorum.isResolved()) {
Mono.just(ctx.callQuorum)
} else {
Mono.error(err)
}
}
.doOnNext {
if (!it.isResolved()) {
log.debug("No quorum for ${ctx.payload.method} as ${ctx.callQuorum}")
}
}
.filter { it.isResolved() }
.map { .map {
val result = it.getResult() ctx.withPayload(it.value)
?: throw CallFailure(ctx.id, Exception("No response from upstream for ${ctx.payload.method}"))
ctx.withPayload(result)
} }
.onErrorMap { .onErrorMap {
log.error("Failed to make a call", it) log.error("Failed to make a call", it)
@@ -204,7 +166,7 @@ open class NativeCall(
} }
open class CallContext<T>(val id: Int, open class CallContext<T>(val id: Int,
val upstream: AggregatedUpstream<*>, val upstream: Multistream,
val matcher: Selector.Matcher, val matcher: Selector.Matcher,
val callQuorum: CallQuorum, val callQuorum: CallQuorum,
val payload: T) { val payload: T) {
@@ -212,8 +174,8 @@ open class NativeCall(
return CallContext(id, upstream, matcher, callQuorum, payload) return CallContext(id, upstream, matcher, callQuorum, payload)
} }
fun getApis(): ApiSource<*> { fun getApis(): ApiSource {
return upstream.getApis(matcher) return upstream.getApiSource(matcher)
} }
} }

View File

@@ -21,10 +21,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -33,7 +31,7 @@ import reactor.core.publisher.Mono
@Service @Service
class StreamHead( class StreamHead(
@Autowired private val upstreams: Upstreams @Autowired private val multistreamHolder: MultistreamHolder
) { ) {
private val log = LoggerFactory.getLogger(StreamHead::class.java) private val log = LoggerFactory.getLogger(StreamHead::class.java)
@@ -42,7 +40,7 @@ class StreamHead(
return requestMono.map { request -> return requestMono.map { request ->
Chain.byId(request.type.number) Chain.byId(request.type.number)
}.flatMapMany { chain -> }.flatMapMany { chain ->
val up = upstreams.getUpstream(chain) val up = multistreamHolder.getUpstream(chain)
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain")) ?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
up.getHead() up.getHead()
.getFlux() .getFlux()
@@ -64,7 +62,7 @@ class StreamHead(
return BlockchainOuterClass.ChainHead.newBuilder() return BlockchainOuterClass.ChainHead.newBuilder()
.setChainValue(chain.id) .setChainValue(chain.id)
.setHeight(block.height) .setHeight(block.height)
.setTimestamp(block.timestamp!!.toEpochMilli()) .setTimestamp(block.timestamp.toEpochMilli())
.setWeight(ByteString.copyFrom(block.difficulty.toByteArray())) .setWeight(ByteString.copyFrom(block.difficulty.toByteArray()))
.setBlockId(block.hash.toHex()) .setBlockId(block.hash.toHex())
.build() .build()

View File

@@ -27,13 +27,13 @@ import reactor.core.publisher.Mono
@Service @Service
class SubscribeStatus( class SubscribeStatus(
@Autowired private val upstreams: Upstreams @Autowired private val multistreamHolder: MultistreamHolder
) { ) {
fun subscribeStatus(requestMono: Mono<BlockchainOuterClass.StatusRequest>): Flux<BlockchainOuterClass.ChainStatus> { fun subscribeStatus(requestMono: Mono<BlockchainOuterClass.StatusRequest>): Flux<BlockchainOuterClass.ChainStatus> {
return requestMono.flatMapMany { return requestMono.flatMapMany {
val ups = upstreams.getAvailable().mapNotNull { chain -> val ups = multistreamHolder.getAvailable().mapNotNull { chain ->
val chainUpstream = upstreams.getUpstream(chain) val chainUpstream = multistreamHolder.getUpstream(chain)
chainUpstream?.observeStatus()?.map { avail -> chainUpstream?.observeStatus()?.map { avail ->
ChainSubscription(chain, chainUpstream, avail) ChainSubscription(chain, chainUpstream, avail)
} }
@@ -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 -> val available = ups.map { u ->
u.getStatus() u.getStatus()
}.min() ?: UpstreamAvailability.UNAVAILABLE }.min() ?: UpstreamAvailability.UNAVAILABLE
@@ -60,6 +60,6 @@ class SubscribeStatus(
.build() .build()
} }
class ChainSubscription(val chain: Chain, val up: AggregatedUpstream<*>, val avail: UpstreamAvailability) class ChainSubscription(val chain: Chain, val up: Multistream, val avail: UpstreamAvailability)
} }

View File

@@ -19,9 +19,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -35,7 +34,7 @@ import kotlin.collections.HashMap
@Service @Service
class TrackBitcoinAddress( class TrackBitcoinAddress(
@Autowired private val upstreams: Upstreams @Autowired private val multistreamHolder: MultistreamHolder
) : TrackAddress { ) : TrackAddress {
companion object { companion object {
@@ -43,7 +42,7 @@ class TrackBitcoinAddress(
} }
override fun isSupported(chain: Chain): Boolean { override fun isSupported(chain: Chain): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain) return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
} }
fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? { fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? {
@@ -63,8 +62,8 @@ class TrackBitcoinAddress(
} }
} }
fun requestBalances(chain: Chain, api: DirectBitcoinApi, addresses: List<String>): Flux<AddressBalance> { fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: List<String>): Flux<AddressBalance> {
return api.executeAndResult(0, "listunspent", emptyList(), List::class.java) return api.getReader().listUnspent()
.flatMapMany { unspents -> .flatMapMany { unspents ->
val result = getTotal(chain, addresses, unspents) val result = getTotal(chain, addresses, unspents)
Flux.fromIterable(result) Flux.fromIterable(result)
@@ -73,17 +72,14 @@ class TrackBitcoinAddress(
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java) val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
return Flux.empty() return Flux.empty()
} }
val result = upstream.getApi(Selector.empty).flatMapMany { api -> return requestBalances(chain, upstream, addresses)
requestBalances(chain, api, addresses) .map(this@TrackBitcoinAddress::buildResponse)
.map(this@TrackBitcoinAddress::buildResponse)
}
return result
} }
fun getTotal(chain: Chain, addresses: List<String>, unspents: List<*>): List<AddressBalance> { fun getTotal(chain: Chain, addresses: List<String>, unspents: List<*>): List<AddressBalance> {
@@ -122,20 +118,16 @@ class TrackBitcoinAddress(
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java) val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
return Flux.empty() return Flux.empty()
} }
val initial = upstream.getApi(Selector.empty).flatMapMany { api -> val initial = requestBalances(chain, upstream, addresses)
requestBalances(chain, api, addresses)
}
val following = upstream.getHead().getFlux() val following = upstream.getHead().getFlux()
.flatMap { block -> .flatMap { block ->
upstream.getApi(Selector.empty).flatMapMany { api -> requestBalances(chain, upstream, addresses)
requestBalances(chain, api, addresses)
}
} }
val last = HashMap<Address, BigInteger>() val last = HashMap<Address, BigInteger>()
val result = Flux.merge(initial, following) val result = Flux.merge(initial, following)

View File

@@ -20,10 +20,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -39,7 +37,7 @@ import kotlin.math.min
@Service @Service
class TrackBitcoinTx( class TrackBitcoinTx(
@Autowired private val upstreams: Upstreams @Autowired private val multistreamHolder: MultistreamHolder
) : TrackTx { ) : TrackTx {
companion object { companion object {
@@ -47,36 +45,35 @@ class TrackBitcoinTx(
} }
override fun isSupported(chain: Chain): Boolean { override fun isSupported(chain: Chain): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain) return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
} }
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> { override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
val chain = Chain.byId(request.chainValue) val chain = Chain.byId(request.chainValue)
val upstream = upstreams.getUpstream(chain)?.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java) val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(chain)) ?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
val txid = request.txId val txid = request.txId
val confirmations = max(min(1, request.confirmationLimit), 12) val confirmations = max(min(1, request.confirmationLimit), 12)
return upstream.getApi(Selector.empty).flatMapMany { api -> return subscribe(chain, upstream, txid)
subscribe(chain, api, upstream, txid) .takeUntil { tx ->
}.takeUntil { tx -> tx.confirmations >= confirmations
tx.confirmations >= confirmations }.map(this::asProto)
}.map(this::asProto)
} }
fun subscribe(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> { fun subscribe(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux<TxStatus> {
return loadExisting(api, txid) return loadExisting(upstream, txid)
.flatMapMany { status -> .flatMapMany { status ->
if (status.mined) { if (status.mined) {
//Head almost always knows the current height, so it can continue with calculating confirmations //Head almost always knows the current height, so it can continue with calculating confirmations
//without publishing an empty TxStatus first //without publishing an empty TxStatus first
continueWithMined(api, upstream, status) continueWithMined(upstream, status)
} else { } else {
loadMempool(upstream, txid) loadMempool(upstream, txid)
.flatMapMany { tx -> .flatMapMany { tx ->
val next = if (tx.found) { val next = if (tx.found) {
untilMined(upstream, tx) untilMined(upstream, tx)
} else { } else {
untilFound(chain, api, upstream, txid) untilFound(chain, upstream, txid)
} }
//fist provide the current status, then updates //fist provide the current status, then updates
Flux.concat(Mono.just(tx), next) Flux.concat(Mono.just(tx), next)
@@ -85,8 +82,8 @@ class TrackBitcoinTx(
} }
} }
fun continueWithMined(api: DirectBitcoinApi, upstream: BitcoinUpstream, status: TxStatus): Flux<TxStatus> { fun continueWithMined(upstream: BitcoinMultistream, status: TxStatus): Flux<TxStatus> {
return api.getBlock(status.blockHash!!) return upstream.getReader().getBlock(status.blockHash!!)
.map { block -> .map { block ->
TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block)) TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block))
}.flatMapMany { tx -> }.flatMapMany { tx ->
@@ -94,41 +91,40 @@ class TrackBitcoinTx(
} }
} }
fun untilFound(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> { fun untilFound(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux<TxStatus> {
return Flux.interval(Duration.ofSeconds(1)) return Flux.interval(Duration.ofSeconds(1))
.take(Duration.ofMinutes(10)) .take(Duration.ofMinutes(10))
.flatMap { loadMempool(upstream, txid) } .flatMap { loadMempool(upstream, txid) }
.skipUntil { it.found } .skipUntil { it.found }
.flatMap { subscribe(chain, api, upstream, txid) } .flatMap { subscribe(chain, upstream, txid) }
.doOnError { t -> .doOnError { t ->
log.error("Failed to wait until found", t) log.error("Failed to wait until found", t)
} }
} }
fun untilMined(upstream: BitcoinUpstream, tx: TxStatus): Mono<TxStatus> { fun untilMined(upstream: BitcoinMultistream, tx: TxStatus): Mono<TxStatus> {
return upstream.getHead().getFlux().flatMap { return upstream.getHead().getFlux().flatMap {
upstream.getApi(Selector.empty).flatMap { api -> loadExisting(upstream, tx.txid)
loadExisting(api, tx.txid) .filter { it.mined }
}.filter { it.mined }
}.single() }.single()
} }
fun withConfirmations(upstream: BitcoinUpstream, tx: TxStatus): Flux<TxStatus> { fun withConfirmations(upstream: BitcoinMultistream, tx: TxStatus): Flux<TxStatus> {
return upstream.getHead().getFlux().map { return upstream.getHead().getFlux().map {
tx.withHead(it.height) tx.withHead(it.height)
} }
} }
fun loadExisting(api: DirectBitcoinApi, txid: String): Mono<TxStatus> { fun loadExisting(api: BitcoinMultistream, txid: String): Mono<TxStatus> {
val mined = api.getTx(txid) val mined = api.getReader().getTx(txid)
return mined.map { return mined.map {
val block = it["blockhash"] as String? val block = it["blockhash"] as String?
TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it)) TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it))
} }
} }
fun loadMempool(upstream: BitcoinUpstream, txid: String): Mono<TxStatus> { fun loadMempool(upstream: BitcoinMultistream, txid: String): Mono<TxStatus> {
val mempool = upstream.getData().getMempool().get() val mempool = upstream.getReader().getMempool().get()
return mempool.map { return mempool.map {
if (it.contains(txid)) { if (it.contains(txid)) {
TxStatus(txid, found = true, mined = false) TxStatus(txid, found = true, mined = false)

View File

@@ -21,9 +21,8 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.Wei import io.infinitape.etherjar.domain.Wei
@@ -35,13 +34,13 @@ import reactor.core.publisher.Mono
@Service @Service
class TrackEthereumAddress( class TrackEthereumAddress(
@Autowired private val upstreams: Upstreams @Autowired private val multistreamHolder: MultistreamHolder
) : TrackAddress { ) : TrackAddress {
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java) private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
override fun isSupported(chain: Chain): Boolean { override fun isSupported(chain: Chain): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain) return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
} }
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
@@ -52,7 +51,7 @@ class TrackEthereumAddress(
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val head = upstreams.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty() val head = multistreamHolder.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
val balances = initAddress(request) val balances = initAddress(request)
.flatMap { tracked -> .flatMap { tracked ->
val current = getBalance(tracked) val current = getBalance(tracked)
@@ -87,14 +86,14 @@ class TrackEthereumAddress(
} }
} }
fun getUpstream(chain: Chain): AggregatedEthereumUpstreams { fun getUpstream(chain: Chain): EthereumMultistream {
return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java) return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain) ?: throw SilentException.UnsupportedBlockchain(chain)
} }
private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux<TrackedAddress> { private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux<TrackedAddress> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
if (!upstreams.isAvailable(chain)) { if (!multistreamHolder.isAvailable(chain)) {
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
} }
if (request.asset.code?.toLowerCase() != "ether") { if (request.asset.code?.toLowerCase() != "ether") {

View File

@@ -23,10 +23,8 @@ import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.domain.TransactionId
@@ -50,7 +48,7 @@ import kotlin.math.min
@Service @Service
class TrackEthereumTx( class TrackEthereumTx(
@Autowired private val upstreams: Upstreams @Autowired private val multistreamHolder: MultistreamHolder
) : TrackTx { ) : TrackTx {
companion object { companion object {
@@ -65,7 +63,7 @@ class TrackEthereumTx(
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java) private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
override fun isSupported(chain: Chain): Boolean { override fun isSupported(chain: Chain): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain) return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
} }
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> { override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
@@ -85,12 +83,12 @@ class TrackEthereumTx(
} }
fun getUpstream(chain: Chain): AggregatedEthereumUpstreams { fun getUpstream(chain: Chain): EthereumMultistream {
return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java) return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain) ?: throw SilentException.UnsupportedBlockchain(chain)
} }
fun subscribe(base: TxDetails, up: AggregatedEthereumUpstreams): Flux<TxDetails> { fun subscribe(base: TxDetails, up: EthereumMultistream): Flux<TxDetails> {
var latestTx = base var latestTx = base
val untilFound = Mono.just(latestTx) val untilFound = Mono.just(latestTx)
@@ -215,7 +213,7 @@ class TrackEthereumTx(
} }
} }
fun updateFromBlock(upstream: AggregatedEthereumUpstreams, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> { fun updateFromBlock(upstream: EthereumMultistream, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> {
return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) { return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) {
val updated = tx.withStatus( val updated = tx.withStatus(
blockHash = blockTx.blockHash, blockHash = blockTx.blockHash,

View File

@@ -19,19 +19,20 @@ package io.emeraldpay.dshackle.startup
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.CurrentUpstreams import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods 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.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.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.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@@ -44,9 +45,10 @@ import kotlin.collections.HashMap
@Repository @Repository
open class ConfiguredUpstreams( open class ConfiguredUpstreams(
@Autowired private val objectMapper: ObjectMapper, @Autowired private val objectMapper: ObjectMapper,
@Autowired private val currentUpstreams: CurrentUpstreams, @Autowired private val currentUpstreams: CurrentMultistreamHolder,
@Autowired private val fileResolver: FileResolver, @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) private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
@@ -134,80 +136,61 @@ open class ConfiguredUpstreams(
options: UpstreamsConfig.Options) { options: UpstreamsConfig.Options) {
val conn = config.connection!! 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) val methods = buildMethods(config, chain)
conn.rpc?.let { endpoint -> val upstream = BitcoinUpstream(config.id
val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) ?: "bitcoin-${seq.getAndIncrement()}", chain, directApi,
rpcApi = DirectBitcoinApi(rpcClient, objectMapper, methods) options, QuorumForLabels.QuorumItem(1, config.labels),
} 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))
}
upstream.start()
currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED))
} }
private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>, private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>,
chain: Chain, chain: Chain,
options: UpstreamsConfig.Options) { options: UpstreamsConfig.Options) {
val conn = config.connection!! 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 urls = ArrayList<URI>()
val methods = buildMethods(config, chain) val methods = buildMethods(config, chain)
conn.rpc?.let { endpoint -> 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) 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 wsFactoryApi: EthereumWsFactory? = conn.ws?.let { endpoint ->
val ethereumUpstream = EthereumUpstream( val wsApi = EthereumWsFactory(
config.id!!, endpoint.url,
chain, rpcApi!!, wsApi, options, endpoint.origin ?: URI("http://localhost"),
QuorumForLabels.QuorumItem(1, config.labels), objectMapper
methods, )
objectMapper) endpoint.basicAuth?.let { auth ->
ethereumUpstream.start() wsApi.basicAuth = auth
currentUpstreams.update(UpstreamChange(chain, ethereumUpstream, UpstreamChange.ChangeType.ADDED)) }
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) { private fun buildGrpcUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options: UpstreamsConfig.Options) {
@@ -230,5 +213,22 @@ open class ConfiguredUpstreams(
.subscribe(currentUpstreams::update) .subscribe(currentUpstreams::update)
} }
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 * Corresponding upstream
*/ */
val upstream: Upstream<*>, val upstream: Upstream,
/** /**
* Type of the change * Type of the change
*/ */

View File

@@ -31,15 +31,24 @@ abstract class AbstractHead : Head {
private val head = AtomicReference<BlockContainer>(null) private val head = AtomicReference<BlockContainer>(null)
private val stream: TopicProcessor<BlockContainer> = TopicProcessor.create() private val stream: TopicProcessor<BlockContainer> = TopicProcessor.create()
private val beforeBlockHandlers = ArrayList<Runnable>()
fun follow(source: Flux<BlockContainer>): Disposable { fun follow(source: Flux<BlockContainer>): Disposable {
return source.distinctUntilChanged { return source
it.hash .distinctUntilChanged {
}.filter { block -> it.hash
val curr = head.get() }.filter { block ->
curr == null || curr.difficulty < block.difficulty val curr = head.get()
} curr == null || curr.difficulty < block.difficulty
}
.doFinally {
// close internal stream if upstream is finished, otherwise it gets stuck
// but technically is should never happen during normal work, only when the Head
// is stopping
stream.onComplete()
}
.subscribe { block -> .subscribe { block ->
notifyBeforeBlock()
val prev = head.getAndUpdate { curr -> val prev = head.getAndUpdate { curr ->
if (curr == null || curr.difficulty < block.difficulty) { if (curr == null || curr.difficulty < block.difficulty) {
block block
@@ -54,6 +63,20 @@ abstract class AbstractHead : Head {
} }
} }
fun notifyBeforeBlock() {
beforeBlockHandlers.forEach { handler ->
try {
handler.run()
} catch (t: Throwable) {
log.warn("Before Block handler error", t)
}
}
}
override fun onBeforeBlock(handler: Runnable) {
beforeBlockHandlers.add(handler)
}
override fun getFlux(): Flux<BlockContainer> { override fun getFlux(): Flux<BlockContainer> {
return Flux.merge( return Flux.merge(
Mono.justOrEmpty(head.get()), Mono.justOrEmpty(head.get()),

View File

@@ -1,122 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.*
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import org.springframework.context.Lifecycle
import reactor.core.Disposable
import reactor.core.publisher.Flux
import java.time.Duration
import java.time.Instant
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import kotlin.concurrent.withLock
/**
* Aggregation of multiple upstreams responding to a single blockchain
*/
abstract class AggregatedUpstream<U : UpstreamApi>(
private val objectMapper: ObjectMapper,
val caches: Caches
) : Upstream<U>, 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>
fun onUpstreamsUpdated() {
reconfigLock.withLock {
getAll().map { it.getMethods() }.let {
callMethods = AggregatedCallMethods(it)
}
}
}
override fun observeStatus(): Flux<UpstreamAvailability> {
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
return Flux.merge(upstreamsFluxes)
.filter(FilterBestAvailability())
.map { it.status }
}
override fun isAvailable(): Boolean {
return getAll().any { it.isAvailable() }
}
override fun getStatus(): UpstreamAvailability {
val upstreams = getAll()
return if (upstreams.isEmpty()) UpstreamAvailability.UNAVAILABLE
else upstreams.map { it.getStatus() }.min()!!
}
override fun getOptions(): UpstreamsConfig.Options {
return UpstreamsConfig.Options()
}
override fun getMethods(): CallMethods {
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
}
override fun start() {
}
override fun stop() {
cacheSubscription?.dispose()
cacheSubscription = null
}
fun onHeadUpdated(head: Head) {
reconfigLock.withLock {
cacheSubscription?.dispose()
cacheSubscription = head.getFlux().subscribe {
caches.cache(Caches.Tag.LATEST, it)
}
cache = CachingEthereumApi(objectMapper, caches, head)
}
}
// --------------------------------------------------------------------------------------------------------
class UpstreamStatus(val upstream: Upstream<UpstreamApi>, val status: UpstreamAvailability, val ts: Instant = Instant.now())
class FilterBestAvailability() : Predicate<UpstreamStatus> {
private val lastRef = AtomicReference<UpstreamStatus>()
override fun test(t: UpstreamStatus): Boolean {
val last = lastRef.get()
val changed = last == null
|| t.status > last.status
|| (last.upstream == t.upstream && t.status != last.status)
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
if (changed) {
lastRef.set(t)
}
return changed
}
}
}

View File

@@ -16,10 +16,12 @@
*/ */
package io.emeraldpay.dshackle.upstream 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 import org.reactivestreams.Publisher
interface ApiSource<U : UpstreamApi> : Publisher<U> { interface ApiSource : Publisher<Upstream> {
fun resolve() fun resolve()
fun request(tries: Int) 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

@@ -1,135 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
import reactor.core.Disposable
import reactor.core.publisher.Mono
import java.lang.IllegalStateException
import java.time.Duration
/**
* General interface to upstream(s) to a single chain
*/
abstract class ChainUpstreams<U : UpstreamApi>(
val chain: Chain,
private val upstreams: MutableList<Upstream<U>>,
caches: Caches,
objectMapper: ObjectMapper
) : AggregatedUpstream<U>(objectMapper, caches), Lifecycle {
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
private var seq = 0
protected var lagObserver: HeadLagObserver<U>? = null
private var subscription: Disposable? = null
open fun init() {
onUpstreamsUpdated()
}
abstract fun updateHead(): Head
abstract fun setHead(head: Head)
override fun getId(): String {
return "!all:${chain.chainCode}"
}
override fun isRunning(): Boolean {
return subscription != null
}
override fun start() {
super.start()
subscription = observeStatus()
.distinctUntilChanged()
.subscribe { printStatus() }
}
override fun stop() {
super.stop()
subscription?.dispose()
subscription = null
getHead().let {
if (it is Lifecycle) {
it.stop()
}
}
lagObserver?.stop()
}
override fun getAll(): List<Upstream<U>> {
return upstreams
}
override fun addUpstream(upstream: Upstream<U>) {
upstreams.add(upstream)
setHead(updateHead())
onUpstreamsUpdated()
}
fun removeUpstream(id: String) {
if (upstreams.removeIf { it.getId() == id }) {
setHead(updateHead())
onUpstreamsUpdated()
}
}
override fun getApis(matcher: Selector.Matcher): ApiSource<U> {
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
}
return FilteredApis(upstreams, matcher, i)
}
override fun getApi(matcher: Selector.Matcher): Mono<U> {
val apis = getApis(matcher)
apis.request(1)
return Mono.from(apis)
.switchIfEmpty(Mono.error<U>(Exception("No API available")))
}
override fun setLag(lag: Long) {
}
override fun getLag(): Long {
return 0
}
fun printStatus() {
var height: Long? = null
try {
height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height
} catch (e: IllegalStateException) {
//timout
} catch (e: Exception) {
log.warn("Head processing error: ${e.javaClass} ${e.message}")
}
val statuses = upstreams.map { it.getStatus() }
.groupBy { it }
.map { "${it.key.name}/${it.value.size}" }
.joinToString(",")
val lag = upstreams.map { it.getLag() }
.joinToString(", ")
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
}
}

View File

@@ -21,14 +21,12 @@ import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.startup.UpstreamChange import io.emeraldpay.dshackle.startup.UpstreamChange
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream 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.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -44,14 +42,14 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@Repository @Repository
class CurrentUpstreams( class CurrentMultistreamHolder(
@Autowired private val objectMapper: ObjectMapper, @Autowired private val objectMapper: ObjectMapper,
@Autowired private val cachesFactory: CachesFactory @Autowired private val cachesFactory: CachesFactory
): Upstreams { ) : MultistreamHolder {
private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java) private val log = LoggerFactory.getLogger(CurrentMultistreamHolder::class.java)
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams<*>>() private val chainMapping = ConcurrentHashMap<Chain, Multistream>()
private val chainsBus = TopicProcessor.create<Chain>() private val chainsBus = TopicProcessor.create<Chain>()
private val callTargets = HashMap<Chain, CallMethods>() private val callTargets = HashMap<Chain, CallMethods>()
private val updateLock = ReentrantLock() private val updateLock = ReentrantLock()
@@ -61,20 +59,18 @@ class CurrentUpstreams(
val chain = change.chain val chain = change.chain
when (BlockchainType.fromBlockchain(chain)) { when (BlockchainType.fromBlockchain(chain)) {
BlockchainType.ETHEREUM -> { BlockchainType.ETHEREUM -> {
val up = change.upstream val up = change.upstream.cast(EthereumUpstream::class.java)
.cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream<EthereumApi> val current = chainMapping[chain] as Multistream?
val current = chainMapping[chain] as ChainUpstreams<EthereumApi>?
val factory = Callable { val factory = Callable {
AggregatedEthereumUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<EthereumApi> EthereumMultistream(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as Multistream
} }
processUpdate(change, up, current, factory) processUpdate(change, up, current, factory)
} }
BlockchainType.BITCOIN -> { BlockchainType.BITCOIN -> {
val up = change.upstream val up = change.upstream.cast(BitcoinUpstream::class.java)
.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java) val current = chainMapping[chain] as Multistream?
val current = chainMapping[chain] as ChainUpstreams<DirectBitcoinApi>?
val factory = Callable { val factory = Callable {
BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<DirectBitcoinApi> BitcoinMultistream(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as Multistream
} }
processUpdate(change, up, current, factory) 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: Multistream?, factory: Callable<Multistream>) {
val chain = change.chain val chain = change.chain
if (change.type == UpstreamChange.ChangeType.REMOVED) { if (change.type == UpstreamChange.ChangeType.REMOVED) {
current?.removeUpstream(up.getId()) current?.removeUpstream(up.getId())
@@ -113,7 +109,7 @@ class CurrentUpstreams(
} }
} }
override fun getUpstream(chain: Chain): AggregatedUpstream<*>? { override fun getUpstream(chain: Chain): Multistream? {
return chainMapping[chain] return chainMapping[chain]
} }

View File

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

View File

@@ -25,4 +25,7 @@ class EmptyHead : Head {
override fun getFlux(): Flux<BlockContainer> { override fun getFlux(): Flux<BlockContainer> {
return Flux.empty() return Flux.empty()
} }
override fun onBeforeBlock(handler: Runnable) {
}
} }

View File

@@ -16,6 +16,9 @@
*/ */
package io.emeraldpay.dshackle.upstream 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 org.reactivestreams.Subscriber
import reactor.core.publisher.EmitterProcessor import reactor.core.publisher.EmitterProcessor
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -26,28 +29,28 @@ import kotlin.math.pow
import kotlin.math.roundToLong import kotlin.math.roundToLong
import kotlin.random.Random import kotlin.random.Random
class FilteredApis<U : UpstreamApi>( class FilteredApis(
allUpstreams: List<Upstream<U>>, allUpstreams: List<Upstream>,
private val matcher: Selector.Matcher, private val matcher: Selector.Matcher,
pos: Int, pos: Int,
private val repeatLimit: Long, private val repeatLimit: Long,
jitter: Int jitter: Int
) : ApiSource<U> { ) : ApiSource {
companion object { companion object {
private const val DEFAULT_DELAY_STEP = 100 private const val DEFAULT_DELAY_STEP = 100
private const val MAX_WAIT_MILLIS = 5000L private const val MAX_WAIT_MILLIS = 5000L
} }
constructor(allUpstreams: List<Upstream<U>>, constructor(allUpstreams: List<Upstream>,
matcher: Selector.Matcher, matcher: Selector.Matcher,
pos: Int) : this(allUpstreams, matcher, pos, 10, 7) 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) matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
private val delay: Int private val delay: Int
private val upstreams: List<Upstream<UpstreamApi>> private val upstreams: List<Upstream>
private val control = EmitterProcessor.create<Boolean>(32, false) private val control = EmitterProcessor.create<Boolean>(32, false)
@@ -75,18 +78,18 @@ class FilteredApis<U : UpstreamApi>(
return Duration.ofMillis(time) return Duration.ofMillis(time)
} }
override fun subscribe(subscriber: Subscriber<in U>) { override fun subscribe(subscriber: Subscriber<in Upstream>) {
val first = Flux.fromIterable(upstreams) val first = Flux.fromIterable(upstreams)
val retries = (1 until repeatLimit).map { r -> val retries = (1 until repeatLimit).map { r ->
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r)) Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
}.let { Flux.concat(it) } }.let { Flux.concat(it) }
Flux.concat(first, retries) Flux.concat(first, retries)
.filter(Upstream<UpstreamApi>::isAvailable) .filter(Upstream::isAvailable)
.filter(matcher::matches) .filter(matcher::matches)
.flatMap { it.getApi(matcher) } .zipWith(control)
.zipWith(control).map { it.t1 } .map { it.t1 }
.subscribe(subscriber as Subscriber<in UpstreamApi>) .subscribe(subscriber)
} }
override fun resolve() { override fun resolve() {

View File

@@ -20,6 +20,20 @@ import io.emeraldpay.dshackle.data.BlockContainer
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
/**
* Subscription to listen to updates to the head of a blockchain.
*/
interface Head { interface Head {
/**
* @return stream of all new blocks, starts from the current block (i.e., first item should be available immediately).
*/
fun getFlux(): Flux<BlockContainer> fun getFlux(): Flux<BlockContainer>
/**
* Add handler that is going to be called each time _before_ a new block is submitted to stream of new blocks.
* Supposed to be used for cleanup/preparation before actual block data will come, to avoid race condition.
* @see getFlux
*/
fun onBeforeBlock(handler: Runnable)
} }

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 * Observer group of upstreams and defined a distance in blocks (lag) between a leader (best height/difficulty) and
* other upstreams. * other upstreams.
*/ */
abstract class HeadLagObserver<A : UpstreamApi>( abstract class HeadLagObserver(
private val master: Head, private val master: Head,
private val followers: Collection<Upstream<A>> private val followers: Collection<Upstream>
) : Lifecycle { ) : Lifecycle {
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java) 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) return Flux.fromIterable(followers)
.parallel(followers.size) .parallel(followers.size)
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) } .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) } .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 return blocks
.map { extractDistance(top, it) } .map { extractDistance(top, it) }
.takeUntil { lag -> lag <= 0L } .takeUntil { lag -> lag <= 0L }

View File

@@ -0,0 +1,235 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.cache.*
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
import reactor.core.Disposable
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
import java.time.Instant
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import kotlin.concurrent.withLock
/**
* Aggregation of multiple upstreams responding to a single blockchain
*/
abstract class Multistream(
val chain: Chain,
private val upstreams: MutableList<Upstream>,
val caches: Caches
) : Upstream, Lifecycle {
companion object {
private val log = LoggerFactory.getLogger(Multistream::class.java)
}
private var cacheSubscription: Disposable? = null
private val reconfigLock = ReentrantLock()
private var callMethods: CallMethods? = null
private var seq = 0
protected var lagObserver: HeadLagObserver? = null
private var subscription: Disposable? = null
open fun init() {
onUpstreamsUpdated()
}
/**
* Get list of all underlying upstreams
*/
fun getAll(): List<Upstream> {
return upstreams
}
/**
* Add an upstream
*/
fun addUpstream(upstream: Upstream) {
upstreams.add(upstream)
setHead(updateHead())
onUpstreamsUpdated()
}
fun removeUpstream(id: String) {
if (upstreams.removeIf { it.getId() == id }) {
setHead(updateHead())
onUpstreamsUpdated()
}
}
/**
* Get a source for direct APIs
*/
fun getApiSource(matcher: Selector.Matcher): ApiSource {
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
}
return FilteredApis(upstreams, matcher, i)
}
/**
* Finds an API that executed directly on a remote.
*/
fun getDirectApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
val apis = getApiSource(matcher)
apis.request(1)
return Mono.from(apis)
.map(Upstream::getApi)
.switchIfEmpty(Mono.error(Exception("No API available for $chain")))
}
/**
* Finds an API that leverages caches and other optimizations/transformations of the request.
*/
abstract fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>>
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
throw NotImplementedError("Immediate direct API is not implemented for Aggregated Upstream")
}
fun onUpstreamsUpdated() {
reconfigLock.withLock {
getAll().map { it.getMethods() }.let {
callMethods = AggregatedCallMethods(it)
}
}
}
override fun observeStatus(): Flux<UpstreamAvailability> {
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
return Flux.merge(upstreamsFluxes)
.filter(FilterBestAvailability())
.map { it.status }
}
override fun isAvailable(): Boolean {
return getAll().any { it.isAvailable() }
}
override fun getStatus(): UpstreamAvailability {
val upstreams = getAll()
return if (upstreams.isEmpty()) UpstreamAvailability.UNAVAILABLE
else upstreams.map { it.getStatus() }.min()!!
}
override fun getOptions(): UpstreamsConfig.Options {
return UpstreamsConfig.Options()
}
override fun getMethods(): CallMethods {
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
}
override fun start() {
subscription = observeStatus()
.distinctUntilChanged()
.subscribe { printStatus() }
}
override fun stop() {
cacheSubscription?.dispose()
cacheSubscription = null
subscription?.dispose()
subscription = null
getHead().let {
if (it is Lifecycle) {
it.stop()
}
}
lagObserver?.stop()
}
fun onHeadUpdated(head: Head) {
reconfigLock.withLock {
cacheSubscription?.dispose()
cacheSubscription = head.getFlux().subscribe {
caches.cache(Caches.Tag.LATEST, it)
}
}
}
abstract fun updateHead(): Head
abstract fun setHead(head: Head)
override fun getId(): String {
return "!all:${chain.chainCode}"
}
override fun isRunning(): Boolean {
return subscription != null
}
override fun setLag(lag: Long) {
}
override fun getLag(): Long {
return 0
}
fun printStatus() {
var height: Long? = null
try {
height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height
} catch (e: java.lang.IllegalStateException) {
//timout
} catch (e: Exception) {
log.warn("Head processing error: ${e.javaClass} ${e.message}")
}
val statuses = upstreams.map { it.getStatus() }
.groupBy { it }
.map { "${it.key.name}/${it.value.size}" }
.joinToString(",")
val lag = upstreams.map { it.getLag() }
.joinToString(", ")
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
}
// --------------------------------------------------------------------------------------------------------
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
class FilterBestAvailability() : Predicate<UpstreamStatus> {
private val lastRef = AtomicReference<UpstreamStatus>()
override fun test(t: UpstreamStatus): Boolean {
val last = lastRef.get()
val changed = last == null
|| t.status > last.status
|| (last.upstream == t.upstream && t.status != last.status)
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
if (changed) {
lastRef.set(t)
}
return changed
}
}
}

View File

@@ -20,8 +20,11 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
interface Upstreams { /**
fun getUpstream(chain: Chain): AggregatedUpstream<*>? * Holds Multistreams configured for a chain.
*/
interface MultistreamHolder {
fun getUpstream(chain: Chain): Multistream?
fun getAvailable(): List<Chain> fun getAvailable(): List<Chain>
fun observeChains(): Flux<Chain> fun observeChains(): Flux<Chain>
fun getDefaultMethods(chain: Chain): CallMethods fun getDefaultMethods(chain: Chain): CallMethods

View File

@@ -96,13 +96,13 @@ class Selector {
} }
interface Matcher { interface Matcher {
fun matches(up: Upstream<UpstreamApi>): Boolean fun matches(up: Upstream): Boolean
} }
class MultiMatcher( class MultiMatcher(
private val matchers: Collection<Matcher> private val matchers: Collection<Matcher>
): Matcher { ): Matcher {
override fun matches(up: Upstream<UpstreamApi>): Boolean { override fun matches(up: Upstream): Boolean {
return matchers.all { it.matches(up) } return matchers.all { it.matches(up) }
} }
@@ -114,13 +114,13 @@ class Selector {
class MethodMatcher( class MethodMatcher(
val method: String val method: String
): Matcher { ): Matcher {
override fun matches(up: Upstream<UpstreamApi>): Boolean { override fun matches(up: Upstream): Boolean {
return up.getMethods().isAllowed(method) return up.getMethods().isAllowed(method)
} }
} }
abstract class LabelSelectorMatcher: Matcher { abstract class LabelSelectorMatcher: Matcher {
override fun matches(up: Upstream<UpstreamApi>): Boolean { override fun matches(up: Upstream): Boolean {
return up.getLabels().any(this::matches) return up.getLabels().any(this::matches)
} }
@@ -129,7 +129,7 @@ class Selector {
} }
class EmptyMatcher: Matcher { class EmptyMatcher: Matcher {
override fun matches(up: Upstream<UpstreamApi>): Boolean { override fun matches(up: Upstream): Boolean {
return true return true
} }
} }
@@ -144,7 +144,7 @@ class Selector {
return null return null
} }
override fun matches(up: Upstream<UpstreamApi>): Boolean { override fun matches(up: Upstream): Boolean {
return true return true
} }
} }

View File

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

View File

@@ -18,24 +18,29 @@ package io.emeraldpay.dshackle.upstream.bitcoin
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.EmptyReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
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 io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.publisher.Mono
class BitcoinChainUpstreams( open class BitcoinMultistream(
chain: Chain, chain: Chain,
val upstreams: MutableList<BitcoinUpstream>, val upstreams: MutableList<BitcoinUpstream>,
caches: Caches, caches: Caches,
objectMapper: ObjectMapper private val objectMapper: ObjectMapper
) : ChainUpstreams<DirectBitcoinApi>(chain, upstreams as MutableList<Upstream<DirectBitcoinApi>>, caches, objectMapper) { ) : Multistream(chain, upstreams as MutableList<Upstream>, caches), Lifecycle {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java) private val log = LoggerFactory.getLogger(BitcoinMultistream::class.java)
} }
private var head: Head? = null private var head: Head? = null
private var reader = BitcoinReader(this, EmptyHead(), objectMapper)
override fun init() { override fun init() {
if (upstreams.size > 0) { if (upstreams.size > 0) {
@@ -68,8 +73,18 @@ class BitcoinChainUpstreams(
return head return head
} }
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
//TODO
return Mono.just(EmptyReader())
}
open fun getReader(): BitcoinReader {
return reader
}
override fun setHead(head: Head) { override fun setHead(head: Head) {
this.head = head this.head = head
reader = BitcoinReader(this, head, objectMapper)
} }
override fun getHead(): Head { override fun getHead(): Head {
@@ -80,18 +95,24 @@ class BitcoinChainUpstreams(
return upstreams.flatMap { it.getLabels() } return upstreams.flatMap { it.getLabels() }
} }
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> { override fun <T : Upstream> cast(selfType: Class<T>): T {
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 {
if (!selfType.isAssignableFrom(this.javaClass)) { if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") 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 package io.emeraldpay.dshackle.upstream.bitcoin
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Head 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.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.cast
open class BitcoinReader( open class BitcoinReader(
api: DirectBitcoinApi, private val upstreams: BitcoinMultistream,
head: Head head: Head,
private val objectMapper: ObjectMapper
) : Lifecycle { ) : Lifecycle {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinReader::class.java) 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 { open fun getMempool(): CachingMempoolData {
return mempool 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 { override fun isRunning(): Boolean {
return mempool.isRunning return mempool.isRunning
} }
@@ -45,4 +64,14 @@ open class BitcoinReader(
override fun stop() { override fun stop() {
mempool.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

@@ -16,8 +16,11 @@
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.AbstractHead import io.emeraldpay.dshackle.upstream.AbstractHead
import io.emeraldpay.dshackle.upstream.Head 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.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import org.springframework.scheduling.concurrent.CustomizableThreadFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory
@@ -29,7 +32,7 @@ import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
class BitcoinRpcHead( class BitcoinRpcHead(
private val api: DirectBitcoinApi, private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
private val extractBlock: ExtractBlock, private val extractBlock: ExtractBlock,
private val interval: Duration = Duration.ofSeconds(15) private val interval: Duration = Duration.ofSeconds(15)
) : Head, AbstractHead(), Lifecycle { ) : Head, AbstractHead(), Lifecycle {
@@ -53,12 +56,14 @@ class BitcoinRpcHead(
val base = Flux.interval(interval) val base = Flux.interval(interval)
.publishOn(scheduler) .publishOn(scheduler)
.flatMap { .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"))) .timeout(Defaults.timeout, Mono.error(Exception("Best block hash is not received")))
} }
.distinctUntilChanged() .distinctUntilChanged()
.flatMap { hash -> .flatMap { hash ->
api.execute(0, "getblock", listOf(hash)) api.read(JsonRpcRequest("getblock", listOf(hash)))
.flatMap(JsonRpcResponse::requireResult)
.map(extractBlock::extract) .map(extractBlock::extract)
.timeout(Defaults.timeout, Mono.error(Exception("Block data is not received"))) .timeout(Defaults.timeout, Mono.error(Exception("Block data is not received")))
} }

View File

@@ -17,9 +17,12 @@ package io.emeraldpay.dshackle.upstream.bitcoin
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.calls.CallMethods 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 io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
@@ -29,12 +32,12 @@ import reactor.core.publisher.Mono
open class BitcoinUpstream( open class BitcoinUpstream(
id: String, id: String,
val chain: Chain, val chain: Chain,
private val api: DirectBitcoinApi, private val directApi: Reader<JsonRpcRequest, JsonRpcResponse>,
options: UpstreamsConfig.Options, options: UpstreamsConfig.Options,
val node: QuorumForLabels.QuorumItem, val node: QuorumForLabels.QuorumItem,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
callMethods: CallMethods callMethods: CallMethods
) : DefaultUpstream<DirectBitcoinApi>(id, options, callMethods), Lifecycle { ) : DefaultUpstream(id, options, callMethods), Lifecycle {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
@@ -42,43 +45,31 @@ open class BitcoinUpstream(
private val head: Head = createHead() private val head: Head = createHead()
private var validatorSubscription: Disposable? = null private var validatorSubscription: Disposable? = null
private val data = BitcoinReader(api, head)
private fun createHead(): Head { private fun createHead(): Head {
return BitcoinRpcHead( return BitcoinRpcHead(
api, directApi,
ExtractBlock(objectMapper) ExtractBlock(objectMapper)
) )
} }
open fun getData(): BitcoinReader {
return data
}
override fun getHead(): Head { override fun getHead(): Head {
return head return head
} }
override fun getApi(matcher: Selector.Matcher): Mono<out DirectBitcoinApi> { override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
return Mono.just(api) return directApi
} }
override fun getLabels(): Collection<UpstreamsConfig.Labels> { override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return listOf(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)) { if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") 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(DirectBitcoinApi::class.java)) {
throw ClassCastException("Cannot cast ${DirectBitcoinApi::class.java} to $apiType")
}
return this as Upstream<A>
} }
override fun isRunning(): Boolean { override fun isRunning(): Boolean {
@@ -86,7 +77,7 @@ open class BitcoinUpstream(
if (head is Lifecycle) { if (head is Lifecycle) {
runningAny = runningAny || head.isRunning runningAny = runningAny || head.isRunning
} }
runningAny = runningAny || data.isRunning runningAny = runningAny
return runningAny return runningAny
} }
@@ -97,7 +88,6 @@ open class BitcoinUpstream(
head.start() head.start()
} }
} }
data.start()
validatorSubscription?.dispose() validatorSubscription?.dispose()
@@ -105,7 +95,7 @@ open class BitcoinUpstream(
this.setLag(0) this.setLag(0)
this.setStatus(UpstreamAvailability.OK) this.setStatus(UpstreamAvailability.OK)
} else { } else {
val validator = BitcoinUpstreamValidator(api, getOptions()) val validator = BitcoinUpstreamValidator(directApi, getOptions())
validatorSubscription = validator.start() validatorSubscription = validator.start()
.subscribe(this::setStatus) .subscribe(this::setStatus)
} }
@@ -115,9 +105,7 @@ open class BitcoinUpstream(
if (head is Lifecycle) { if (head is Lifecycle) {
head.stop() head.stop()
} }
data.stop()
validatorSubscription?.dispose() validatorSubscription?.dispose()
} }
} }

View File

@@ -16,7 +16,10 @@
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.UpstreamAvailability 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.slf4j.LoggerFactory
import org.springframework.scheduling.concurrent.CustomizableThreadFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -26,7 +29,7 @@ import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
class BitcoinUpstreamValidator( class BitcoinUpstreamValidator(
private val api: DirectBitcoinApi, private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
private val options: UpstreamsConfig.Options private val options: UpstreamsConfig.Options
) { ) {
@@ -36,7 +39,9 @@ class BitcoinUpstreamValidator(
} }
fun validate(): Mono<UpstreamAvailability> { 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 -> .map { count ->
val minPeers = options.minPeers ?: 1 val minPeers = options.minPeers ?: 1
if (count < minPeers) { if (count < minPeers) {

View File

@@ -15,7 +15,11 @@
*/ */
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.bitcoin
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Head 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.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.Disposable import reactor.core.Disposable
@@ -26,8 +30,9 @@ import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
open class CachingMempoolData( open class CachingMempoolData(
private val api: DirectBitcoinApi, private val upstreams: BitcoinMultistream,
private val head: Head private val head: Head,
private val objectMapper: ObjectMapper
) : Lifecycle { ) : Lifecycle {
companion object { companion object {
@@ -56,7 +61,11 @@ open class CachingMempoolData(
} }
fun fetchFromUpstream(): Mono<List<String>> { 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>) { 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"), getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"),
false, false,
json, json,
data,
transactions transactions
) )
} }

View File

@@ -40,7 +40,7 @@ class AggregatedCallMethods(
*/ */
override fun getQuorumFor(method: String): CallQuorum { override fun getQuorumFor(method: String): CallQuorum {
return delegates.find { return delegates.find {
it.isAllowed(method) it.isAllowed(method) || it.isHardcoded(method)
}?.getQuorumFor(method) ?: throw IllegalStateException("No quorum for $method") }?.getQuorumFor(method) ?: throw IllegalStateException("No quorum for $method")
} }
@@ -62,15 +62,15 @@ class AggregatedCallMethods(
* @return true if there is at least one delegate that allows the method and it's hardcoded on that delegate * @return true if there is at least one delegate that allows the method and it's hardcoded on that delegate
*/ */
override fun isHardcoded(method: String): Boolean { override fun isHardcoded(method: String): Boolean {
return delegates.any { it.isAllowed(method) && it.isHardcoded(method) } return delegates.any { it.isHardcoded(method) }
} }
/** /**
* Executed the method on the first delegate that supports it as a hardcoded method * 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 { return delegates.find {
it.isAllowed(method) && it.isHardcoded(method) it.isHardcoded(method)
}?.executeHardcoded(method) ?: throw IllegalStateException("No hardcoded for $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 * 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.calls
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.quorum.* 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 io.infinitape.etherjar.rpc.RpcException
import java.util.* import java.util.*
@@ -26,9 +24,6 @@ class DefaultBitcoinMethods(
private val objectMapper: ObjectMapper private val objectMapper: ObjectMapper
) : CallMethods { ) : CallMethods {
//TODO maybe Ethereum RPC parser should not be really used for Bitcoin
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
private val anyResponseMethods = listOf( private val anyResponseMethods = listOf(
"getblock", "getblock",
"gettransaction", "getrawtransaction", "gettxout", "gettransaction", "getrawtransaction", "gettxout",
@@ -55,7 +50,7 @@ class DefaultBitcoinMethods(
Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum() Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum()
Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2) Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2)
Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0) Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0)
Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(jacksonRpcConverter) Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(objectMapper)
else -> AlwaysQuorum() else -> AlwaysQuorum()
} }
} }
@@ -72,13 +67,10 @@ class DefaultBitcoinMethods(
return Collections.binarySearch(hardcodedMethods, method) >= 0; return Collections.binarySearch(hardcodedMethods, method) >= 0;
} }
override fun executeHardcoded(method: String): Any { override fun executeHardcoded(method: String): ByteArray {
return when (method) { return when (method) {
"getconnectioncount" -> 42 "getconnectioncount" -> "42".toByteArray()
"getnetworkinfo" -> mapOf( "getnetworkinfo" -> "{\"version\": 700000, \"subversion\": \"/EmeraldDshackle:v0.7/\"}".toByteArray()
"version" to 700000,
"subversion" to "/EmeraldDshackle:v0.7/"
)
else -> throw RpcException(-32601, "Method not found") else -> throw RpcException(-32601, "Method not found")
} }
} }

View File

@@ -32,8 +32,6 @@ class DefaultEthereumMethods(
private val chain: Chain private val chain: Chain
) : CallMethods { ) : CallMethods {
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
private val anyResponseMethods = listOf( private val anyResponseMethods = listOf(
"eth_gasPrice", "eth_gasPrice",
"eth_call", "eth_call",
@@ -90,9 +88,9 @@ class DefaultEthereumMethods(
headVerifiedMethods.contains(method) -> NotLaggingQuorum(1) headVerifiedMethods.contains(method) -> NotLaggingQuorum(1)
specialMethods.contains(method) -> { specialMethods.contains(method) -> {
when (method) { when (method) {
"eth_getTransactionCount" -> NonceQuorum(jacksonRpcConverter) "eth_getTransactionCount" -> NonceQuorum(objectMapper)
"eth_getBalance" -> NotLaggingQuorum(1) "eth_getBalance" -> NotLaggingQuorum(1)
"eth_sendRawTransaction" -> BroadcastQuorum(jacksonRpcConverter) "eth_sendRawTransaction" -> BroadcastQuorum(objectMapper)
else -> AlwaysQuorum() else -> AlwaysQuorum()
} }
} }
@@ -108,50 +106,55 @@ class DefaultEthereumMethods(
return hardcodedMethods.contains(method) return hardcodedMethods.contains(method)
} }
override fun executeHardcoded(method: String): Any { override fun executeHardcoded(method: String): ByteArray {
if ("net_version" == method) { val json = when (method) {
if (Chain.ETHEREUM == chain) { "net_version" -> {
return "1" 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) { "net_peerCount" -> {
return "1" "\"0x2a\""
} }
if (Chain.TESTNET_MORDEN == chain) { "net_listening" -> {
return "2" "true"
} }
if (Chain.TESTNET_KOVAN == chain) { "web3_clientVersion" -> {
return "42" "\"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 json.toByteArray()
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")
} }
override fun getSupportedMethods(): Set<String> { override fun getSupportedMethods(): Set<String> {

View File

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

View File

@@ -55,7 +55,7 @@ class ManagedCallMethods(
return delegate.isHardcoded(method) return delegate.isHardcoded(method)
} }
override fun executeHardcoded(method: String): Any { override fun executeHardcoded(method: String): ByteArray {
return delegate.executeHardcoded(method) 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

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package io.emeraldpay.dshackle.cache package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
@@ -30,20 +30,20 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
/** /**
* Reads blocks with full transactions details. Based on data contained in cashes for blocks * Reads blocks with full transactions details. Based on data contained in readers for blocks
* and transactions, i.e. two separate caches that must be provided. * 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 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 * If any of the expected block transactions is not available it returns empty
*/ */
class EthereumBlocksWithTxCache( class EthereumFullBlocksReader(
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val blocks: Reader<BlockId, BlockContainer>, private val blocks: Reader<BlockId, BlockContainer>,
private val txes: Reader<TxId, TxContainer> private val txes: Reader<TxId, TxContainer>
) : Reader<BlockId, BlockContainer> { ) : Reader<BlockId, BlockContainer> {
companion object { 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> { override fun read(key: BlockId): Mono<BlockContainer> {

View File

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

View File

@@ -19,20 +19,24 @@ package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.* 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 io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.publisher.Mono
open class AggregatedEthereumUpstreams( open class EthereumMultistream(
chain: Chain, chain: Chain,
val upstreams: MutableList<EthereumUpstream>, val upstreams: MutableList<EthereumUpstream>,
caches: Caches, caches: Caches,
objectMapper: ObjectMapper private val objectMapper: ObjectMapper
) : ChainUpstreams<EthereumApi>(chain, upstreams as MutableList<Upstream<EthereumApi>>, caches, objectMapper) { ) : Multistream(chain, upstreams as MutableList<Upstream>, caches) {
companion object { companion object {
private val log = LoggerFactory.getLogger(AggregatedEthereumUpstreams::class.java) private val log = LoggerFactory.getLogger(EthereumMultistream::class.java)
} }
private var head: Head? = null private var head: Head? = null
@@ -92,7 +96,7 @@ open class AggregatedEthereumUpstreams(
val newHead = MergedHead(upstreams.map { it.getHead() }).apply { val newHead = MergedHead(upstreams.map { it.getHead() }).apply {
this.start() this.start()
} }
val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection<Upstream<EthereumApi>>).apply { val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection<Upstream>).apply {
this.start() this.start()
} }
this.lagObserver = lagObserver this.lagObserver = lagObserver
@@ -107,18 +111,15 @@ open class AggregatedEthereumUpstreams(
} }
@SuppressWarnings("unchecked") @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)) { if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") 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> { override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
if (!apiType.isAssignableFrom(EthereumApi::class.java)) { return Mono.just(NativeCallRouter(objectMapper, reader, getMethods()))
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as Upstream<A>
} }
} }

View File

@@ -18,24 +18,21 @@ package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.cache.CurrentBlockCache import io.emeraldpay.dshackle.cache.CurrentBlockCache
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.*
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxContainer
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.* import io.emeraldpay.dshackle.reader.*
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.domain.Wei import io.infinitape.etherjar.domain.Wei
import io.infinitape.etherjar.rpc.Commands import io.infinitape.etherjar.hex.HexQuantity
import io.infinitape.etherjar.rpc.RpcCall 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.BlockJson
import io.infinitape.etherjar.rpc.json.BlockTag
import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson import io.infinitape.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -48,7 +45,7 @@ import java.util.concurrent.TimeoutException
import java.util.function.Function import java.util.function.Function
open class EthereumReader( open class EthereumReader(
private val up: Upstream<EthereumApi>, private val up: Multistream,
private val caches: Caches, private val caches: Caches,
private val objectMapper: ObjectMapper private val objectMapper: ObjectMapper
) : Lifecycle { ) : Lifecycle {
@@ -57,51 +54,109 @@ open class EthereumReader(
private val log = LoggerFactory.getLogger(EthereumReader::class.java) private val log = LoggerFactory.getLogger(EthereumReader::class.java)
} }
private var headListener: Disposable? = null
private val balanceCache = CurrentBlockCache<Address, Wei>() private val balanceCache = CurrentBlockCache<Address, Wei>()
private val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { block -> val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { block ->
objectMapper val existing = block.getParsed(BlockJson::class.java)
.readValue(block.json, BlockJson::class.java) if (existing != null) {
.withoutTransactionDetails() existing.withoutTransactionDetails()
} else {
objectMapper
.readValue(block.json, BlockJson::class.java)
.withoutTransactionDetails()
}
} }
private val extractTx = Function<TxContainer, TransactionJson> { tx -> val extractTx = Function<TxContainer, TransactionJson> { tx ->
objectMapper tx.getParsed(TransactionJson::class.java) ?: objectMapper.readValue(tx.json, TransactionJson::class.java)
.readValue(tx.json, TransactionJson::class.java)
} }
private val blocksDirect: Reader<BlockHash, BlockJson<TransactionRefJson>> val asRaw = Function<SourceContainer, ByteArray> { tx ->
private val txDirect: Reader<TransactionId, TransactionJson> 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 balanceDirect: Reader<Address, Wei>
private val idToBlockHash = Function<BlockId, BlockHash> { id -> BlockHash.from(id.value) } private val idToBlockHash = Function<BlockId, BlockHash> { id -> BlockHash.from(id.value) }
private val blockHashToId = Function<BlockHash, BlockId> { hash -> BlockId.from(hash) } private val blockHashToId = Function<BlockHash, BlockId> { hash -> BlockId.from(hash) }
private val txHashToId = Function<TransactionId, TxId> { hash -> TxId.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 { init {
blocksDirect = object : Reader<BlockHash, BlockJson<TransactionRefJson>> { blocksDirect = object : Reader<BlockHash, BlockContainer> {
override fun read(key: BlockHash): Mono<BlockJson<TransactionRefJson>> { override fun read(key: BlockHash): Mono<BlockContainer> {
return up.getApi(Selector.empty).flatMap { api -> return up.getDirectApi(Selector.empty).flatMap { api ->
api.executeAndConvert(Commands.eth().getBlock(key)) val request = JsonRpcRequest("eth_getBlockByHash", listOf(key.toHex(), false))
api.read(request)
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key"))) .timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key")))
.map(directResponseBytes)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) .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 -> .doOnNext { block ->
caches.cache(Caches.Tag.REQUESTED, BlockContainer.from(block, objectMapper)) caches.cache(Caches.Tag.REQUESTED, block)
} }
} }
} }
} }
txDirect = object : Reader<TransactionId, TransactionJson> { blocksByHeightDirect = object : Reader<Long, BlockContainer> {
override fun read(key: TransactionId): Mono<TransactionJson> { override fun read(key: Long): Mono<BlockContainer> {
return up.getApi(Selector.empty).flatMap { api -> return up.getDirectApi(Selector.empty).flatMap { api ->
api.executeAndConvert(Commands.eth().getTransaction(key)) val request = JsonRpcRequest("eth_getBlockByNumber", listOf(HexQuantity.from(key).toHex(), false))
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key"))) api.read(request)
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key")))
.map(directResponseBytes)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) .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 -> .doOnNext { tx ->
if (tx.blockNumber != null && tx.blockHash != null) { if (tx.blockId != null) {
caches.cache(Caches.Tag.REQUESTED, TxContainer.from(tx, objectMapper)) caches.cache(Caches.Tag.REQUESTED, tx)
} }
} }
} }
@@ -109,9 +164,19 @@ open class EthereumReader(
} }
balanceDirect = object : Reader<Address, Wei> { balanceDirect = object : Reader<Address, Wei> {
override fun read(key: Address): Mono<Wei> { override fun read(key: Address): Mono<Wei> {
return up.getApi(Selector.empty).flatMap { api -> return up.getDirectApi(Selector.empty).flatMap { api ->
api.executeAndConvert(Commands.eth().getBalance(key, BlockTag.LATEST)) val request = JsonRpcRequest("eth_getBalance", listOf(key.toHex(), "latest"))
api.read(request)
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Balance not read $key"))) .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))) .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.doOnNext { value -> .doOnNext { value ->
balanceCache.put(key, value) balanceCache.put(key, value)
@@ -121,30 +186,61 @@ open class EthereumReader(
} }
} }
fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> { fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
return CompoundReader( return TransformingReader(
TransformingReader(caches.getBlocksByHash(), extractBlock), CompoundReader(
RekeyingReader(idToBlockHash, blocksDirect) RekeyingReader(blockHashToId, caches.getBlocksByHash()),
blocksDirect
),
extractBlock
) )
} }
fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> { fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> {
return CompoundReader( return TransformingReader(
RekeyingReader( CompoundReader(
blockHashToId, caches.getBlocksByHash(),
TransformingReader(caches.getBlocksByHash(), extractBlock) 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> { fun txByHash(): Reader<TransactionId, TransactionJson> {
return CompoundReader( return TransformingReader(
RekeyingReader( CompoundReader(
txHashToId, RekeyingReader(txHashToId, caches.getTxByHash()),
TransformingReader(caches.getTxByHash(), extractTx) txDirect
), ),
txDirect extractTx
)
}
fun txByHashAsCont(): Reader<TxId, TxContainer> {
return CompoundReader(
caches.getTxByHash(),
RekeyingReader(idToTxHash, txDirect)
) )
} }
@@ -155,18 +251,17 @@ open class EthereumReader(
} }
override fun isRunning(): Boolean { override fun isRunning(): Boolean {
return this.headListener != null //TODO should be always running?
return up.isRunning
} }
override fun start() { override fun start() {
this.headListener = up.getHead().getFlux().subscribe { val evictCaches: Runnable = Runnable {
balanceCache.evict() balanceCache.evict()
} }
up.getHead().onBeforeBlock(evictCaches)
} }
override fun stop() { override fun stop() {
val headListener = this.headListener
this.headListener = null
headListener?.dispose()
} }
} }

View File

@@ -19,6 +19,10 @@ package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.data.BlockContainer 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 io.infinitape.etherjar.rpc.Commands
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
@@ -31,7 +35,7 @@ import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
class EthereumRpcHead( class EthereumRpcHead(
private val api: DirectEthereumApi, private val api: Reader<in JsonRpcRequest, out JsonRpcResponse>,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val interval: Duration = Duration.ofSeconds(10) private val interval: Duration = Duration.ofSeconds(10)
): DefaultEthereumHead(), Lifecycle { ): DefaultEthereumHead(), Lifecycle {
@@ -48,21 +52,27 @@ class EthereumRpcHead(
val base = Flux.interval(interval) val base = Flux.interval(interval)
.publishOn(scheduler) .publishOn(scheduler)
.flatMap { .flatMap {
api.rpcClient api.read(JsonRpcRequest("eth_blockNumber", emptyList()))
.execute(Commands.eth().blockNumber)
.subscribeOn(scheduler) .subscribeOn(scheduler)
.timeout(Defaults.timeout, Mono.error(Exception("Block number not received"))) .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 { .flatMap {
//fetching by Block Height here, critical to use same upstream, //fetching by Block Height here, critical to use same upstream,
//different upstreams may have different blocks on the same height //different upstreams may have different blocks on the same height
api.rpcClient api.read(JsonRpcRequest("eth_getBlockByNumber", listOf(it.toHex(), false)))
.execute(Commands.eth().getBlock(it))
.subscribeOn(scheduler) .subscribeOn(scheduler)
.timeout(Defaults.timeout, Mono.error(Exception("Block data not received"))) .timeout(Defaults.timeout, Mono.error(Exception("Block data not received")))
} }
.map { .map {
BlockContainer.from(it, objectMapper) BlockContainer.from(it.getResult(), objectMapper)
} }
.onErrorContinue { err, _ -> .onErrorContinue { err, _ ->
log.debug("RPC error ${err.message}") 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.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods 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 io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
@@ -34,15 +37,15 @@ import java.time.Duration
open class EthereumUpstream( open class EthereumUpstream(
id: String, id: String,
val chain: Chain, val chain: Chain,
private val api: DirectEthereumApi, private val directReader: Reader<JsonRpcRequest, JsonRpcResponse>,
private val ethereumWs: EthereumWs? = null, private val ethereumWsFactory: EthereumWsFactory? = null,
options: UpstreamsConfig.Options, options: UpstreamsConfig.Options,
val node: QuorumForLabels.QuorumItem, val node: QuorumForLabels.QuorumItem,
targets: CallMethods, targets: CallMethods,
private val objectMapper: ObjectMapper 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()), UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
DirectCallMethods(), objectMapper) DirectCallMethods(), objectMapper)
@@ -52,12 +55,7 @@ open class EthereumUpstream(
private val head: Head = this.createHead() private val head: Head = this.createHead()
private var validatorSubscription: Disposable? = null private var validatorSubscription: Disposable? = null
init {
api.upstream = this
}
override fun setCaches(caches: Caches) { override fun setCaches(caches: Caches) {
api.caches = caches;
if (head is CachesEnabled) { if (head is CachesEnabled) {
head.setCaches(caches) head.setCaches(caches)
} }
@@ -70,7 +68,7 @@ open class EthereumUpstream(
this.setLag(0) this.setLag(0)
this.setStatus(UpstreamAvailability.OK) this.setStatus(UpstreamAvailability.OK)
} else { } else {
val validator = EthereumUpstreamValidator(this, getOptions()) val validator = EthereumUpstreamValidator(this, getOptions(), objectMapper)
validatorSubscription = validator.start() validatorSubscription = validator.start()
.subscribe(this::setStatus) .subscribe(this::setStatus)
} }
@@ -89,21 +87,24 @@ open class EthereumUpstream(
} }
open fun createHead(): Head { open fun createHead(): Head {
return if (ethereumWs != null) { return if (ethereumWsFactory != null) {
val ws = EthereumWsHead(ethereumWs).apply { val ws = ethereumWsFactory.create(this).apply {
this.start() connect()
} }
// receive bew blocks through Websockets, but periodically verify with RPC val wsHead = EthereumWsHead(ws).apply {
val rpc = EthereumRpcHead(api, objectMapper, Duration.ofSeconds(30)).apply { start()
this.start()
} }
MergedHead(listOf(rpc, ws)).apply { // receive bew blocks through WebSockets, but also periodically verify with RPC in case if WS failed
this.start() val rpcHead = EthereumRpcHead(getApi(), objectMapper, Duration.ofSeconds(60)).apply {
start()
}
MergedHead(listOf(rpcHead, wsHead)).apply {
start()
} }
} else { } else {
log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC") log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC")
EthereumRpcHead(api, objectMapper).apply { EthereumRpcHead(getApi(), objectMapper).apply {
this.start() start()
} }
} }
} }
@@ -112,8 +113,8 @@ open class EthereumUpstream(
return head return head
} }
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> { override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
return Mono.just(api) return directReader
} }
override fun getLabels(): Collection<UpstreamsConfig.Labels> { override fun getLabels(): Collection<UpstreamsConfig.Labels> {
@@ -121,18 +122,11 @@ open class EthereumUpstream(
} }
@Suppress("unchecked") @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)) { if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") 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

@@ -16,11 +16,13 @@
*/ */
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.UpstreamAvailability 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.slf4j.LoggerFactory
import org.springframework.scheduling.concurrent.CustomizableThreadFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -30,8 +32,9 @@ import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
class EthereumUpstreamValidator( class EthereumUpstreamValidator(
private val ethereumUpstream: EthereumUpstream, private val upstream: EthereumUpstream,
private val options: UpstreamsConfig.Options private val options: UpstreamsConfig.Options,
private val objectMapper: ObjectMapper
) { ) {
companion object { companion object {
private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java) private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java)
@@ -39,30 +42,32 @@ class EthereumUpstreamValidator(
} }
fun validate(): Mono<UpstreamAvailability> { fun validate(): Mono<UpstreamAvailability> {
return ethereumUpstream return upstream
.getApi(Selector.empty) .getApi()
.flatMapMany { api -> .read(JsonRpcRequest("eth_syncing", listOf()))
api.rpcClient .flatMap(JsonRpcResponse::requireResult)
.execute(Commands.eth().syncing()) .map { objectMapper.readValue(it, SyncingJson::class.java) }
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing"))) .timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing")))
.flatMap { value -> .flatMap { value ->
if (value.isSyncing) { if (value.isSyncing) {
Mono.just(UpstreamAvailability.SYNCING) Mono.just(UpstreamAvailability.SYNCING)
} else { } else {
api.rpcClient.execute(Commands.net().peerCount()) upstream
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers"))) .getApi()
.map { count -> .read(JsonRpcRequest("net_peerCount", listOf()))
val minPeers = options.minPeers ?: 1 .flatMap(JsonRpcResponse::requireStringResult)
if (count < minPeers) { .map(Integer::decode)
UpstreamAvailability.IMMATURE .timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers")))
} else { .map { count ->
UpstreamAvailability.OK val minPeers = options.minPeers ?: 1
} if (count < minPeers) {
} UpstreamAvailability.IMMATURE
} else {
UpstreamAvailability.OK
}
} }
} }
} }
.single()
.onErrorReturn(UpstreamAvailability.UNAVAILABLE) .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 package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.Disposable import reactor.core.Disposable
class EthereumWsHead( class EthereumWsHead(
private val ws: EthereumWs private val ws: EthereumWsFactory.EthereumWs
): DefaultEthereumHead(), Lifecycle, CachesEnabled { ) : DefaultEthereumHead(), Lifecycle {
private val log = LoggerFactory.getLogger(EthereumWsHead::class.java) private val log = LoggerFactory.getLogger(EthereumWsHead::class.java)
@@ -43,8 +41,4 @@ class EthereumWsHead(
subscription = null subscription = null
} }
override fun setCaches(caches: Caches) {
ws.setCaches(caches)
}
} }

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.hex.HexQuantity
import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.RpcResponseError
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import java.math.BigInteger
class NativeCallRouter(
private val objectMapper: ObjectMapper,
private val reader: EthereumReader,
private val methods: CallMethods
) : Reader<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.isHardcoded(key.method)) {
return Mono.just(methods.executeHardcoded(key.method))
.map { JsonRpcResponse(it, null) }
}
if (!methods.isAllowed(key.method)) {
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
}
val common = commonRequests(key)
if (common != null) {
return common.map { JsonRpcResponse(it, null) }
}
return Mono.empty()
}
/**
* Prepare RpcCall with data types specific for that particular requests. In general it may return a call that just
* parses JSON into Map. But the purpose of further processing and caching for some of the requests we want
* to have actual data types.
*/
fun commonRequests(key: JsonRpcRequest): Mono<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

@@ -22,22 +22,21 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead import io.emeraldpay.dshackle.upstream.ethereum.*
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
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 io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.rpc.* import io.infinitape.etherjar.rpc.*
import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.Disposable import reactor.core.Disposable
@@ -58,16 +57,15 @@ open class EthereumGrpcUpstream(
private val chain: Chain, private val chain: Chain,
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub, private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val rpcClient: ReactorEmeraldClient private val client: JsonRpcGrpcClient
) : DefaultUpstream<EthereumApi>( ) : DefaultUpstream(
"$parentId/${chain.chainCode}", "$parentId/${chain.chainCode}",
UpstreamsConfig.Options.getDefaults(), UpstreamsConfig.Options.getDefaults(),
null null
), CachesEnabled, Lifecycle { ), Lifecycle {
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>() private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java) private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
private var caches: Caches? = null
private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels()) private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels())
private val head = DefaultEthereumHead() private val head = DefaultEthereumHead()
@@ -76,16 +74,7 @@ open class EthereumGrpcUpstream(
var timeout = Defaults.timeout var timeout = Defaults.timeout
open fun createApi(matcher: Selector.Matcher): DirectEthereumApi { private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
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
}
}
override fun start() { override fun start() {
if (this.isRunning) return if (this.isRunning) return
@@ -123,6 +112,7 @@ open class EthereumGrpcUpstream(
BigInteger(1, value.weight.toByteArray()), BigInteger(1, value.weight.toByteArray()),
Instant.ofEpochMilli(value.timestamp), Instant.ofEpochMilli(value.timestamp),
false, false,
null,
null null
) )
block block
@@ -132,9 +122,11 @@ open class EthereumGrpcUpstream(
val curr = head.getCurrent() val curr = head.getCurrent()
curr == null || curr.difficulty < block.difficulty curr == null || curr.difficulty < block.difficulty
}.flatMap { }.flatMap {
getApi(Selector.EmptyMatcher()) defaultReader.read(JsonRpcRequest("eth_getBlockByHash", listOf(it.hash.toHexWithPrefix(), false)))
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(BlockHash(it.hash.value))) } .flatMap(JsonRpcResponse::requireResult)
.map { BlockContainer.from(it, objectMapper) } .map {
BlockContainer.from(it, objectMapper)
}
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream"))) .timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
.doOnError { t -> .doOnError { t ->
setStatus(UpstreamAvailability.UNAVAILABLE) setStatus(UpstreamAvailability.UNAVAILABLE)
@@ -208,26 +200,16 @@ open class EthereumGrpcUpstream(
return head return head
} }
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> { override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
return Mono.just(createApi(matcher)) return defaultReader
}
override fun setCaches(caches: Caches) {
this.caches = caches
} }
@SuppressWarnings("unchecked") @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)) { if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") 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

@@ -25,10 +25,10 @@ import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.startup.UpstreamChange import io.emeraldpay.dshackle.startup.UpstreamChange
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.grpc.ManagedChannelBuilder import io.grpc.ManagedChannelBuilder
import io.grpc.netty.NettyChannelBuilder import io.grpc.netty.NettyChannelBuilder
import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient
import io.netty.handler.ssl.* import io.netty.handler.ssl.*
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.exception.ExceptionUtils
@@ -57,7 +57,6 @@ class GrpcUpstreams(
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
private val known = HashMap<Chain, EthereumGrpcUpstream>() private val known = HashMap<Chain, EthereumGrpcUpstream>()
private val lock = ReentrantLock() private val lock = ReentrantLock()
private var grpcTransport: ReactorEmeraldClient? = null
fun start(): Flux<UpstreamChange> { fun start(): Flux<UpstreamChange> {
val channel: ManagedChannelBuilder<*> = if (auth != null && StringUtils.isNotEmpty(auth.ca)) { val channel: ManagedChannelBuilder<*> = if (auth != null && StringUtils.isNotEmpty(auth.ca)) {
@@ -74,10 +73,6 @@ class GrpcUpstreams(
val client = ReactorBlockchainGrpc.newReactorStub(channel.build()) val client = ReactorBlockchainGrpc.newReactorStub(channel.build())
this.client = client this.client = client
this.grpcTransport = ReactorEmeraldClient.newBuilder()
.connectUsing(client.channel)
.objectMapper(objectMapper)
.build()
val statusSubscription = AtomicReference<Disposable>() val statusSubscription = AtomicReference<Disposable>()
@@ -162,7 +157,8 @@ class GrpcUpstreams(
lock.withLock { lock.withLock {
val current = known[chain] val current = known[chain]
return if (current == null) { 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 created.timeout = this.timeout
known[chain] = created known[chain] = created
created.start() 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

@@ -13,27 +13,41 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.reader.Reader
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaders import io.netty.handler.codec.http.HttpHeaders
import io.netty.handler.ssl.SslContextBuilder
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.netty.http.client.HttpClient 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.*
import java.util.function.Consumer import java.util.function.Consumer
open class BitcoinRpcClient( /**
* JSON RPC client
*/
class JsonRpcHttpClient(
private val target: String, private val target: String,
basicAuth: AuthConfig.ClientBasicAuth? private val objectMapper: ObjectMapper,
) { basicAuth: AuthConfig.ClientBasicAuth? = null,
tlsCAAuth: ByteArray? = null
) : Reader<JsonRpcRequest, JsonRpcResponse> {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinRpcClient::class.java) private val log = LoggerFactory.getLogger(JsonRpcHttpClient::class.java)
} }
private val parser = JsonRpcParser()
private val httpClient: HttpClient private val httpClient: HttpClient
init { init {
@@ -43,14 +57,27 @@ open class BitcoinRpcClient(
h.add(HttpHeaderNames.CONTENT_TYPE, "application/json") h.add(HttpHeaderNames.CONTENT_TYPE, "application/json")
} }
basicAuth?.let { basicAuth -> basicAuth?.let { auth ->
val authString: String = basicAuth.username + ":" + basicAuth.password val authString: String = auth.username + ":" + auth.password
val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray()) val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray())
val auth = "Basic $authBase64" val encodedAuth = "Basic $authBase64"
val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, auth) } val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, encodedAuth) }
build = build.headers(headers) build = build.headers(headers)
} }
tlsCAAuth?.let { auth ->
val cf = CertificateFactory.getInstance("X.509")
val cert = cf.generateCertificate(ByteArrayInputStream(auth)) as X509Certificate
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
ks.load(null, "".toCharArray())
ks.setCertificateEntry("server", cert)
val sslContext = SslContextBuilder.forClient().trustManager(cert).build()
build.secure { spec ->
spec.sslContext(sslContext)
}
}
this.httpClient = build this.httpClient = build
} }
@@ -65,5 +92,10 @@ open class BitcoinRpcClient(
.asByteArray() .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

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import io.infinitape.etherjar.rpc.RpcResponseError
import org.slf4j.LoggerFactory
class JsonRpcParser() {
companion object {
private val log = LoggerFactory.getLogger(JsonRpcParser::class.java)
}
private val jsonFactory = JsonFactory()
fun parse(json: ByteArray): JsonRpcResponse {
val parser: JsonParser = jsonFactory.createParser(json)
parser.nextToken()
if (parser.currentToken != JsonToken.START_OBJECT) {
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON"))
}
var nullResponse: JsonRpcResponse? = null
while (parser.nextToken() != JsonToken.END_OBJECT) {
val field = parser.currentName
if (field == "jsonrpc" || field == "id") {
if (!parser.nextToken().isScalarValue) {
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id/type)"))
}
// just skip the field
} else if (field == "result") {
val value = parser.nextToken()
val start = parser.tokenLocation
if (value.isScalarValue) {
val text = parser.text
if (value == JsonToken.VALUE_STRING) {
return JsonRpcResponse(("\"" + text + "\"").toByteArray(), null)
} else if (value == JsonToken.VALUE_NULL) {
//if null we should check if error is present
nullResponse = JsonRpcResponse(text.toByteArray(), null)
} else {
return JsonRpcResponse(text.toByteArray(), null)
}
} else if (value == JsonToken.START_OBJECT || value == JsonToken.START_ARRAY) {
parser.skipChildren()
val end = parser.currentLocation.byteOffset.toInt()
val copy = ByteArray((end - start.byteOffset).toInt())
System.arraycopy(json, start.byteOffset.toInt(), copy, 0, copy.size)
return JsonRpcResponse(copy, null)
}
} else if (field == "error") {
val err = readError(parser)
if (err != null) {
return JsonRpcResponse(null, err)
}
}
}
if (nullResponse != null) {
return nullResponse
}
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure"))
}
fun readError(parser: JsonParser): JsonRpcResponse.ResponseError? {
var code = 0
var message = ""
while (parser.nextToken() != JsonToken.END_OBJECT) {
if (parser.currentToken() == JsonToken.VALUE_NULL) {
// error is just null
return null
}
val field = parser.currentName()
if (field == "code" && parser.currentToken == JsonToken.VALUE_NUMBER_INT) {
code = parser.intValue
} else if (field == "message" && parser.currentToken == JsonToken.VALUE_STRING) {
message = parser.valueAsString
}
}
return JsonRpcResponse.ResponseError(code, message)
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.databind.ObjectMapper
class JsonRpcRequest(
val method: String,
val params: List<Any>
) {
fun toJson(objectMapper: ObjectMapper): ByteArray {
val json = mapOf(
"jsonrpc" to "2.0",
"id" to 1,
"method" to method,
"params" to params
)
return objectMapper.writeValueAsBytes(json)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is JsonRpcRequest) return false
if (method != other.method) return false
if (params != other.params) return false
return true
}
override fun hashCode(): Int {
var result = method.hashCode()
result = 31 * result + params.hashCode()
return result
}
}

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import io.infinitape.etherjar.rpc.RpcException
import reactor.core.publisher.Mono
class JsonRpcResponse(
private val result: ByteArray?,
val error: ResponseError?
) {
companion object {
private val NULL_VALUE = "null".toByteArray()
@JvmStatic
fun ok(value: ByteArray): JsonRpcResponse {
return JsonRpcResponse(value, null)
}
@JvmStatic
fun ok(value: String): JsonRpcResponse {
return JsonRpcResponse(value.toByteArray(), null)
}
@JvmStatic
fun error(code: Int, msg: String): JsonRpcResponse {
return JsonRpcResponse(null, ResponseError(code, msg))
}
}
fun hasResult(): Boolean {
return result != null
}
fun hasError(): Boolean {
return error != null
}
fun isNull(): Boolean {
return result != null && NULL_VALUE.contentEquals(result)
}
fun getResult(): ByteArray {
return result ?: ByteArray(0)
}
fun getResultAsRawString(): String {
return String(getResult())
}
fun getResultAsProcessedString(): String {
val str = getResultAsRawString()
if (str.startsWith("\"") && str.endsWith("\"")) {
return str.substring(1, str.length - 1)
}
throw IllegalStateException("Not as JS string")
}
fun requireResult(): Mono<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
if (result != null) {
if (other.result == null) return false
if (!result.contentEquals(other.result)) return false
} else if (other.result != null) return false
if (error != other.error) return false
return true
}
override fun hashCode(): Int {
var result1 = result?.contentHashCode() ?: 0
result1 = 31 * result1 + (error?.hashCode() ?: 0)
return result1
}
class ResponseError(val code: Int, val message: String) {
fun asException(): RpcException {
return RpcException(code, message)
}
}
class ResponseJsonSerializer : JsonSerializer<JsonRpcResponse>() {
override fun serialize(value: JsonRpcResponse, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("jsonrpc", "2.0")
gen.writeNumberField("id", 0)
if (value.error != null) {
gen.writeObjectFieldStart("error")
gen.writeNumberField("code", value.error.code)
gen.writeStringField("message", value.error.message)
gen.writeEndObject()
} else {
if (value.result == null) {
throw IllegalStateException("No result set")
}
gen.writeRawUTF8String(value.result, 0, value.result.size)
}
gen.writeEndObject()
}
}
}

View File

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

View File

@@ -63,7 +63,8 @@ class TxRedisCacheSpec extends Specification {
2000, 2000,
TxId.from(hash1), TxId.from(hash1),
BlockId.from(hash2), BlockId.from(hash2),
"test".bytes "test".bytes,
null
) )
when: when:
def enc = cache.toProto(cont) 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.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.BroadcastQuorum import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification import spock.lang.Specification
class BroadcastQuorumSpec extends Specification { class BroadcastQuorumSpec extends Specification {
def rpcConverted = TestingCommons.rpcConverter()
def objectMapper = TestingCommons.objectMapper() def objectMapper = TestingCommons.objectMapper()
def "Resolved with first after 3 tries"() { def "Resolved with first after 3 tries"() {
setup: setup:
def q = Spy(new BroadcastQuorum(rpcConverted, 3)) def q = Spy(new BroadcastQuorum(objectMapper, 3))
def upstream1 = Stub(Upstream) def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream) def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream) def upstream3 = Stub(Upstream)
@@ -40,28 +40,28 @@ class BroadcastQuorumSpec extends Specification {
!q.isResolved() !q.isResolved()
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream1) q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream1)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2) q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
when: when:
q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3) q.record(new RpcException(1, "Nonce too low"), upstream3)
then: then:
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
q.isResolved() q.isResolved()
objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"] objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
} }
def "Remembers first response"() { def "Remembers first response"() {
setup: setup:
def q = Spy(new BroadcastQuorum(rpcConverted, 3)) def q = Spy(new BroadcastQuorum(objectMapper, 3))
def upstream1 = Stub(Upstream) def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream) def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream) def upstream3 = Stub(Upstream)
@@ -72,22 +72,22 @@ class BroadcastQuorumSpec extends Specification {
!q.isResolved() !q.isResolved()
when: when:
q.record(objectMapper.writeValueAsBytes([error: [message: "Internal error"]]), upstream1) q.record(new RpcException(1, "Internal error"), upstream1)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2) q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
when: when:
q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3) q.record(new RpcException(1, "Nonce too low"), upstream3)
then: then:
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
q.isResolved() q.isResolved()
objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"] objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
} }
} }

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification
class NonEmptyQuorumSpec extends Specification {
def "Fail if too many errors"() {
setup:
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream1)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream2)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream3)
then:
q.isFailed()
!q.isResolved()
}
def "Fail first if not error"() {
setup:
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record('"0x11"'.bytes, upstream1)
then:
q.isResolved()
!q.isFailed()
}
def "Fail second if first is error"() {
setup:
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream1)
then:
!q.isFailed()
!q.isResolved()
when:
q.record('"0x11"'.bytes, upstream2)
then:
q.isResolved()
!q.isFailed()
}
def "Fail second if first is null"() {
setup:
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record('null'.bytes, upstream2)
then:
!q.isFailed()
!q.isResolved()
when:
q.record('"0x11"'.bytes, upstream2)
then:
q.isResolved()
!q.isFailed()
}
}

View File

@@ -20,16 +20,16 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.NonceQuorum import io.emeraldpay.dshackle.quorum.NonceQuorum
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification import spock.lang.Specification
class NonceQuorumSpec extends Specification { class NonceQuorumSpec extends Specification {
def rpcConverted = TestingCommons.rpcConverter()
def objectMapper = TestingCommons.objectMapper() def objectMapper = TestingCommons.objectMapper()
def "Gets max value"() { def "Gets max value"() {
setup: setup:
def q = Spy(new NonceQuorum(rpcConverted, 3)) def q = Spy(new NonceQuorum(objectMapper, 3))
def upstream1 = Stub(Upstream) def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream) def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream) def upstream3 = Stub(Upstream)
@@ -40,28 +40,28 @@ class NonceQuorumSpec extends Specification {
!q.isResolved() !q.isResolved()
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream1) q.record('"0x10"'.bytes, upstream1)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordValue(_, "0x10", _) 1 * q.recordValue(_, "0x10", _)
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2) q.record('"0x11"'.bytes, upstream2)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordValue(_, "0x11", _) 1 * q.recordValue(_, "0x11", _)
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3) q.record('"0x10"'.bytes, upstream3)
then: then:
1 * q.recordValue(_, "0x10", _) 1 * q.recordValue(_, "0x10", _)
q.isResolved() q.isResolved()
objectMapper.readValue(q.result, Map) == [result: "0x11"] objectMapper.readValue(q.result, Object) == "0x11"
} }
def "Ignores errors"() { def "Ignores errors"() {
setup: setup:
def q = Spy(new NonceQuorum(rpcConverted, 3)) def q = Spy(new NonceQuorum(objectMapper, 3))
def upstream1 = Stub(Upstream) def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream) def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream) def upstream3 = Stub(Upstream)
@@ -72,28 +72,60 @@ class NonceQuorumSpec extends Specification {
!q.isResolved() !q.isResolved()
when: when:
q.record(objectMapper.writeValueAsBytes([error: [error: "Internal"]]), upstream1) q.record(new RpcException(1, "Internal"), upstream1)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2) q.record('"0x11"'.bytes, upstream2)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordValue(_, "0x11", _) 1 * q.recordValue(_, "0x11", _)
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3) q.record('"0x10"'.bytes, upstream3)
then: then:
1 * q.recordValue(_, "0x10", _) 1 * q.recordValue(_, "0x10", _)
!q.isResolved() !q.isResolved()
when: when:
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream1) q.record('"0x11"'.bytes, upstream1)
then: then:
1 * q.recordValue(_, "0x11", _) 1 * q.recordValue(_, "0x11", _)
q.isResolved() q.isResolved()
objectMapper.readValue(q.result, Map) == [result: "0x11"] objectMapper.readValue(q.result, Object) == "0x11"
}
def "Fail if too many errors"() {
setup:
def q = Spy(new NonceQuorum(objectMapper, 3))
def upstream1 = Stub(Upstream)
def upstream2 = Stub(Upstream)
def upstream3 = Stub(Upstream)
when:
q.init(Stub(Head))
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream1)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream2)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream3)
then:
q.isFailed()
!q.isResolved()
} }
} }

View File

@@ -0,0 +1,206 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.FilteredApis
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
class QuorumRpcReaderSpec extends Specification {
def "always-quorum - get the result if ok"() {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
1 * getApi() >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(JsonRpcResponse.ok("1"))
}
}
def apis = new FilteredApis(
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new AlwaysQuorum())
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectNext("1")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "always-quorum - retry upstream error"() {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getApi() >> Mock(Reader) {
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.error(1, "test")),
Mono.just(JsonRpcResponse.ok("1"))
]
}
}
def apis = new FilteredApis(
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new AlwaysQuorum())
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectNext("1")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "non-empty-quorum - get the second result if first is null"() {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getApi() >> Mock(Reader) {
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.ok("null")),
Mono.just(JsonRpcResponse.ok("1"))
]
}
}
def apis = new FilteredApis(
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectNext("1")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "non-empty-quorum - get the second result if first is error"() {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getApi() >> Mock(Reader) {
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.error(1, "test")),
Mono.just(JsonRpcResponse.ok("1"))
]
}
}
def apis = new FilteredApis(
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectNext("1")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "non-empty-quorum - get the third result if first two are not ok"() {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getApi() >> Mock(Reader) {
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.ok("null")),
Mono.just(JsonRpcResponse.error(1, "test")),
Mono.just(JsonRpcResponse.ok("1"))
]
}
}
def apis = new FilteredApis(
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectNext("1")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "non-empty-quorum - no result if all failed"() {
setup:
def up = Mock(Upstream) {
_ * isAvailable() >> true
_ * getApi() >> Mock(Reader) {
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
Mono.just(JsonRpcResponse.ok("null")),
Mono.just(JsonRpcResponse.error(1, "test")),
Mono.just(JsonRpcResponse.ok("null"))
]
}
}
def apis = new FilteredApis(
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectComplete()
.verify(Duration.ofSeconds(1))
}
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable
import spock.lang.Specification
class ValueAwareQuorumSpec extends Specification {
def "Extract null"() {
setup:
def quorum = new ValueAwareQuorumImpl()
when:
def act = quorum.extractValue("null".bytes, Object)
then:
act == null
}
def "Extract string"() {
setup:
def quorum = new ValueAwareQuorumImpl()
when:
def act = quorum.extractValue("\"foo\"".bytes, Object)
then:
act == "foo"
}
def "Extract number"() {
setup:
def quorum = new ValueAwareQuorumImpl()
when:
def act = quorum.extractValue("100".bytes, Object)
then:
act == 100
}
def "Extract map"() {
setup:
def quorum = new ValueAwareQuorumImpl()
when:
def act = quorum.extractValue("{\"foo\": 1}".bytes, Object)
then:
act == [foo: 1]
}
class ValueAwareQuorumImpl extends ValueAwareQuorum {
ValueAwareQuorumImpl() {
super(TestingCommons.objectMapper(), Object)
}
@Override
void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @NotNull Upstream upstream) {
}
@Override
void recordError(@Nullable byte[] response, @Nullable String errorMessage, @NotNull Upstream upstream) {
}
@Override
void init(@NotNull Head head) {
}
@Override
boolean isResolved() {
return false
}
@Override
byte[] getResult() {
return new byte[0]
}
@Override
boolean isFailed() {
return false
}
}
}

View File

@@ -19,21 +19,25 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.quorum.BroadcastQuorum import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
import io.emeraldpay.dshackle.quorum.QuorumRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.quorum.AlwaysQuorum 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.quorum.NonEmptyQuorum
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.ethereum.NativeCallRouter
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.ReactorRpcClient import io.infinitape.etherjar.rpc.ReactorRpcClient
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.RpcResponseError
import io.infinitape.etherjar.rpc.RpcResponseException
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.test.StepVerifier import reactor.test.StepVerifier
import spock.lang.Ignore
import spock.lang.Specification import spock.lang.Specification
import java.time.Duration import java.time.Duration
@@ -43,140 +47,99 @@ class NativeCallSpec extends Specification {
def objectMapper = TestingCommons.objectMapper() def objectMapper = TestingCommons.objectMapper()
def "Tries router first"() {
def routedApi = Mock(Reader) {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null))
}
def upstream = Mock(Multistream) {
1 * getRoutedApi(_) >> Mono.just(routedApi)
}
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(
1, upstream, Selector.empty, new AlwaysQuorum(),
new NativeCall.ParsedCallDetails("eth_test", [])
)
when:
def act = nativeCall.fetch(ctx).block(Duration.ofSeconds(1))
then:
act.payload == "1".bytes
}
def "Return error if router denied the requests"() {
def routedApi = Mock(Reader) {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message"))
}
def upstream = Mock(Multistream) {
1 * getRoutedApi(_) >> Mono.just(routedApi)
}
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(
15, upstream, Selector.empty, new AlwaysQuorum(),
new NativeCall.ParsedCallDetails("eth_test", [])
)
when:
def act = nativeCall.fetch(ctx) //.block(Duration.ofSeconds(1))
then:
StepVerifier.create(act)
.expectErrorMatches { t ->
t instanceof NativeCall.CallFailure &&
t.id == 15 &&
t.reason instanceof RpcException &&
t.reason.rpcMessage == "Test message"
}
.verify(Duration.ofSeconds(1))
}
def "Quorum is applied"() { def "Quorum is applied"() {
setup: setup:
def quorum = Spy(new AlwaysQuorum()) def quorum = new AlwaysQuorum()
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
apiMock.answer("eth_test", [], "foo") def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper())
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) 1 * create(_, _) >> Mock(Reader) {
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), 1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, 1))
Selector.empty, quorum, }
}
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", [])) new NativeCall.ParsedCallDetails("eth_test", []))
when: when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2)) def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))
def act = objectMapper.readValue(resp.payload, Map) def act = objectMapper.readValue(resp.payload, Object)
then: 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 upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
apiMock.answerOnce("eth_test", [], null)
apiMock.answerOnce("eth_test", [], "bar")
apiMock.answerOnce("eth_test", [], null)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", []))
when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def act = objectMapper.readValue(resp.payload, Map)
then:
act == [jsonrpc:"2.0", id:1, result: "bar"]
2 * quorum.record(_, _)
1 * quorum.getResult()
}
def "Have pause between repeats"() {
setup:
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
apiMock.answerOnce("eth_test", [], null)
apiMock.answerOnce("eth_test", [], "bar")
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", []))
when:
def t1 = System.currentTimeMillis()
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def delta = System.currentTimeMillis() - t1
then:
delta > 95 // should be 100, but sometimes gives less ???
new String(resp.payload) == '{"jsonrpc":"2.0","id":1,"result":"bar"}'
}
def "One call has no pause"() {
setup:
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
apiMock.answerOnce("eth_test", [], "bar")
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", []))
when:
def t1 = System.currentTimeMillis()
nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def delta = System.currentTimeMillis() - t1
then:
delta < 50
} }
def "Returns error if no quorum"() { def "Returns error if no quorum"() {
setup: setup:
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3)) def quorum = new AlwaysQuorum()
def upstreams = Stub(Upstreams) def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper())
ReactorRpcClient rpcClient = Stub(ReactorRpcClient) nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
def apiMock = TestingCommons.api(rpcClient) 1 * create(_, _) >> Mock(Reader) {
apiMock.upstream = Stub(Upstream) 1 * read(_) >> Mono.empty()
}
apiMock.answer("eth_test", [], null, 3) }
apiMock.answerOnce("eth_test", [], "foo") def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum,
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", [])) new NativeCall.ParsedCallDetails("eth_test", []))
3 * quorum.record(_, _)
1 * quorum.getResult()
when: when:
def resp = nativeCall.executeOnRemote(call) def resp = nativeCall.executeOnRemote(call)
then: then:
StepVerifier.create(resp) StepVerifier.create(resp)
.expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 1}) .expectErrorMatches({ t -> t instanceof NativeCall.CallFailure && t.id == 1 })
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
def "Packs call exception into response with id"() { def "Packs call exception into response with id"() {
setup: setup:
def upstreams = Stub(Upstreams) def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
when: when:
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test"))) def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
@@ -193,7 +156,7 @@ class NativeCallSpec extends Specification {
def "Packs unknown exception into response"() { def "Packs unknown exception into response"() {
setup: setup:
def upstreams = Stub(Upstreams) def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
when: when:
def resp = nativeCall.processException(new IllegalArgumentException("test test")) def resp = nativeCall.processException(new IllegalArgumentException("test test"))
@@ -209,13 +172,13 @@ class NativeCallSpec extends Specification {
def "Builds normal response"() { def "Builds normal response"() {
setup: setup:
def upstreams = Stub(Upstreams) def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def json = [jsonrpc:"2.0", id:1, result: "foo"] def json = [jsonrpc:"2.0", id:1, result: "foo"]
when: when:
def resp = nativeCall.buildResponse( 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: then:
resp.id == 1561 resp.id == 1561
@@ -225,7 +188,7 @@ class NativeCallSpec extends Specification {
def "Returns error for invalid chain"() { def "Returns error for invalid chain"() {
setup: setup:
def upstreams = Stub(Upstreams) def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def req = BlockchainOuterClass.NativeCallRequest.newBuilder() def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
@@ -248,7 +211,7 @@ class NativeCallSpec extends Specification {
def "Returns error for unsupported chain"() { def "Returns error for unsupported chain"() {
setup: setup:
def upstreams = Mock(Upstreams) def upstreams = Mock(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def req = BlockchainOuterClass.NativeCallRequest.newBuilder() def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
@@ -270,14 +233,14 @@ class NativeCallSpec extends Specification {
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
@Ignore
//TODO
def "Calls cache before remote"() { def "Calls cache before remote"() {
setup: setup:
def upstreams = Stub(Upstreams) def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def api = Mock(DirectEthereumApi) def api = TestingCommons.api()
def upstream = TestingCommons.aggregatedUpstream(api) def upstream = TestingCommons.aggregatedUpstream(api)
def cacheMock = Mock(CachingEthereumApi)
upstream.cache = cacheMock
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10, def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
upstream, upstream,
@@ -289,13 +252,13 @@ class NativeCallSpec extends Specification {
1 * cacheMock.execute(10, "eth_test", []) >> Mono.empty() 1 * cacheMock.execute(10, "eth_test", []) >> Mono.empty()
} }
@Ignore
//TODO
def "Uses cached value"() { def "Uses cached value"() {
setup: setup:
def upstreams = Stub(Upstreams) def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper()) def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def upstream = TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi)) def upstream = TestingCommons.aggregatedUpstream(TestingCommons.api())
def cacheMock = Mock(CachingEthereumApi)
upstream.cache = cacheMock
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10, def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
upstream, upstream,
@@ -307,63 +270,4 @@ class NativeCallSpec extends Specification {
1 * cacheMock.execute(10, "eth_test", []) >> Mono.just('{"result": "foo"}'.bytes) 1 * cacheMock.execute(10, "eth_test", []) >> Mono.just('{"result": "foo"}'.bytes)
new String(act.block().payload) == '{"result": "foo"}' new String(act.block().payload) == '{"result": "foo"}'
} }
def "Retries on error"() {
setup:
def quorum = Spy(new AlwaysQuorum())
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 1"))
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 2"))
apiMock.answerOnce("eth_test", [], "bar")
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", []))
when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def act = objectMapper.readValue(resp.payload, Map)
then:
act == [jsonrpc:"2.0", id:1, result: "bar"]
1 * quorum.record(_, _)
1 * quorum.getResult()
}
def "Send raw retries 3 times"() {
setup:
def quorum = Spy(new BroadcastQuorum(TestingCommons.rpcConverter(), 3))
def upstreams = Stub(Upstreams)
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
def apiMock = TestingCommons.api(rpcClient)
apiMock.upstream = Stub(Upstream)
apiMock.answer("eth_sendRawTransaction", ["0x1234"],
"0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b", 1)
apiMock.answer("eth_sendRawTransaction", ["0x1234"], null, 10,
new RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported"))
// apiMock.answer("eth_sendRawTransaction", ["0x1234"],
// new RpcResponseError(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported"), 10)
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_sendRawTransaction", ["0x1234"]))
when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
def act = objectMapper.readValue(resp.payload, Map)
then:
act == [jsonrpc:"2.0", id:1, result: "0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b"]
1 * quorum.record(_ as byte[], _)
2 * quorum.record(_ as RpcException, _)
}
} }

View File

@@ -23,13 +23,10 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.test.EthereumUpstreamMock import io.emeraldpay.dshackle.test.EthereumUpstreamMock
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.test.UpstreamsMock import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash 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.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson import io.infinitape.etherjar.rpc.json.TransactionRefJson
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -45,7 +42,7 @@ class StreamHeadSpec extends Specification {
def "Errors on unavailable chain"() { def "Errors on unavailable chain"() {
setup: setup:
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(EthereumUpstream)) def upstreams = new MultistreamHolderMock(Chain.ETHEREUM, Stub(EthereumUpstream))
def streamHead = new StreamHead(upstreams) def streamHead = new StreamHead(upstreams)
when: when:
def flux = streamHead.add( def flux = streamHead.add(
@@ -80,8 +77,8 @@ class StreamHeadSpec extends Specification {
.build() .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 upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstream)
def streamHead = new StreamHead(upstreams) def streamHead = new StreamHead(upstreams)
when: when:
def flux = streamHead.add( def flux = streamHead.add(

View File

@@ -20,11 +20,11 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.AggregatedUpstream import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -43,7 +43,7 @@ class TrackBitcoinAddressSpec extends Specification {
setup: setup:
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json") def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json")
def unspents = TestingCommons.objectMapper().readValue(json, List) def unspents = TestingCommons.objectMapper().readValue(json, List)
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
when: when:
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents) def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
@@ -58,7 +58,7 @@ class TrackBitcoinAddressSpec extends Specification {
setup: setup:
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
def unspents = TestingCommons.objectMapper().readValue(json, List) def unspents = TestingCommons.objectMapper().readValue(json, List)
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
when: when:
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents) def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
@@ -73,7 +73,7 @@ class TrackBitcoinAddressSpec extends Specification {
setup: setup:
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
def unspents = TestingCommons.objectMapper().readValue(json, List) def unspents = TestingCommons.objectMapper().readValue(json, List)
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
when: when:
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address } def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address }
@@ -93,7 +93,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Zero for empty unspents"() { def "Zero for empty unspents"() {
setup: setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
when: when:
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], []) def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], [])
@@ -108,7 +108,7 @@ class TrackBitcoinAddressSpec extends Specification {
setup: setup:
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
def unspents = TestingCommons.objectMapper().readValue(json, List) def unspents = TestingCommons.objectMapper().readValue(json, List)
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
when: when:
def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address } def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address }
@@ -126,7 +126,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "One address for single provided"() { def "One address for single provided"() {
setup: setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def req = BlockchainOuterClass.BalanceRequest.newBuilder() def req = BlockchainOuterClass.BalanceRequest.newBuilder()
.setAddress( .setAddress(
Common.AnyAddress.newBuilder() Common.AnyAddress.newBuilder()
@@ -144,7 +144,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Sorted addresses for multiple provided"() { def "Sorted addresses for multiple provided"() {
setup: setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def req = BlockchainOuterClass.BalanceRequest.newBuilder() def req = BlockchainOuterClass.BalanceRequest.newBuilder()
.setAddress( .setAddress(
Common.AnyAddress.newBuilder() Common.AnyAddress.newBuilder()
@@ -165,7 +165,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Null for no address provided"() { def "Null for no address provided"() {
setup: setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def req = BlockchainOuterClass.BalanceRequest.newBuilder() def req = BlockchainOuterClass.BalanceRequest.newBuilder()
.build() .build()
when: when:
@@ -176,7 +176,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Build proto for common balance"() { def "Build proto for common balance"() {
setup: setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(123456)) def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(123456))
when: when:
def act = track.buildResponse(balance) def act = track.buildResponse(balance)
@@ -189,7 +189,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Build proto for zero balance"() { def "Build proto for zero balance"() {
setup: setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.ZERO) def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.ZERO)
when: when:
def act = track.buildResponse(balance) def act = track.buildResponse(balance)
@@ -202,7 +202,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Build proto for all bitcoins"() { def "Build proto for all bitcoins"() {
setup: setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(21_000_000).multiply(BigInteger.TEN.pow(8))) def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(21_000_000).multiply(BigInteger.TEN.pow(8)))
when: when:
def act = track.buildResponse(balance) def act = track.buildResponse(balance)
@@ -216,24 +216,24 @@ class TrackBitcoinAddressSpec extends Specification {
def "Get update for a balance"() { def "Get update for a balance"() {
setup: setup:
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
2 * executeAndResult(0, "listunspent", [], List) >>> [
Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
]
}
def blocks = TopicProcessor.create() def blocks = TopicProcessor.create()
Head head = Mock(Head) { Head head = Mock(Head) {
1 * getFlux() >> Flux.from(blocks) 1 * getFlux() >> Flux.from(blocks)
} }
Upstream upstream def upstream = null
upstream = Mock(AggregatedUpstream) { upstream = Mock(BitcoinMultistream) {
_ * getApi(_) >> Mono.just(api) _ * getReader() >> Mock(BitcoinReader) {
2 * listUnspent() >>> [
Mono.just([]),
Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
]
}
_ * getHead() >> head _ * getHead() >> head
_ * castApi(_) >> { return upstream } _ * cast(_) >> {
} upstream
Upstreams upstreams = Mock(Upstreams) { }
_ * getUpstream(Chain.BITCOIN) >> upstream
} }
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.BITCOIN, upstream)
TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams) TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams)
when: when:
@@ -253,7 +253,7 @@ class TrackBitcoinAddressSpec extends Specification {
StepVerifier.create(resp) StepVerifier.create(resp)
.expectNext("0") .expectNext("0")
.then { .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") .expectNext("1230000")
.then { .then {

View File

@@ -18,11 +18,9 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader 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.dshackle.upstream.bitcoin.CachingMempoolData
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -37,7 +35,7 @@ class TrackBitcoinTxSpec extends Specification {
def "loadMempool() returns not found when not found"() { def "loadMempool() returns not found when not found"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) { CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
1 * get() >> Mono.just([ 1 * get() >> Mono.just([
@@ -45,8 +43,8 @@ class TrackBitcoinTxSpec extends Specification {
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
]) ])
} }
BitcoinUpstream upstream = Mock(BitcoinUpstream) { BitcoinMultistream upstream = Mock(BitcoinMultistream) {
_ * getData() >> Mock(BitcoinReader) { _ * getReader() >> Mock(BitcoinReader) {
_ * getMempool() >> mempoolAccess _ * getMempool() >> mempoolAccess
} }
} }
@@ -64,15 +62,15 @@ class TrackBitcoinTxSpec extends Specification {
def "loadMempool() returns ok when found"() { def "loadMempool() returns ok when found"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) { CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
1 * get() >> Mono.just([ 1 * get() >> Mono.just([
"69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9",
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
]) ])
} }
BitcoinUpstream upstream = Mock(BitcoinUpstream) { BitcoinMultistream upstream = Mock(BitcoinMultistream) {
_ * getData() >> Mock(BitcoinReader) { _ * getReader() >> Mock(BitcoinReader) {
_ * getMempool() >> mempoolAccess _ * getMempool() >> mempoolAccess
} }
} }
@@ -90,15 +88,17 @@ class TrackBitcoinTxSpec extends Specification {
def "loadExiting() returns not found if not mined"() { def "loadExiting() returns not found if not mined"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) { BitcoinMultistream upstream = Mock(BitcoinMultistream) {
1 * getTx(txid) >> Mono.just([ _ * getReader() >> Mock(BitcoinReader) {
txid: txid 1 * getTx(txid) >> Mono.just([
]) txid: txid
])
}
} }
when: when:
def act = track.loadExisting(api, txid) def act = track.loadExisting(upstream, txid)
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -111,17 +111,19 @@ class TrackBitcoinTxSpec extends Specification {
def "loadExiting() returns block if mined"() { def "loadExiting() returns block if mined"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) { BitcoinMultistream upstream = Mock(BitcoinMultistream) {
1 * getTx(txid) >> Mono.just([ _ * getReader() >> Mock(BitcoinReader) {
txid : txid, 1 * getTx(txid) >> Mono.just([
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", txid : txid,
height : 100 blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
]) height : 100
])
}
} }
when: when:
def act = track.loadExisting(api, txid) def act = track.loadExisting(upstream, txid)
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -136,16 +138,16 @@ class TrackBitcoinTxSpec extends Specification {
def "Goes with confirmations"() { def "Goes with confirmations"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
// start with the current block // start with the current block
def next = Flux.fromIterable([10, 12, 13, 14, 15]).map { h -> 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) { Head head = Mock(Head) {
1 * getFlux() >> next 1 * getFlux() >> next
} }
Upstream upstream = Mock(BitcoinUpstream) { BitcoinMultistream upstream = Mock(BitcoinMultistream) {
1 * getHead() >> head 1 * getHead() >> head
} }
def status = new TrackBitcoinTx.TxStatus( def status = new TrackBitcoinTx.TxStatus(
@@ -167,16 +169,16 @@ class TrackBitcoinTxSpec extends Specification {
def "Wait until mined"() { def "Wait until mined"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
// start with the current block // start with the current block
def next = Flux.fromIterable([10, 12, 13]).map { h -> 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) { Head head = Mock(Head) {
1 * getFlux() >> next 1 * getFlux() >> next
} }
DirectBitcoinApi api = Mock(DirectBitcoinApi) { BitcoinReader api = Mock(BitcoinReader) {
3 * getTx(txid) >>> [ 3 * getTx(txid) >>> [
Mono.just([ Mono.just([
txid: txid txid: txid
@@ -191,9 +193,9 @@ class TrackBitcoinTxSpec extends Specification {
]) ])
] ]
} }
Upstream upstream = Mock(BitcoinUpstream) { BitcoinMultistream upstream = Mock(BitcoinMultistream) {
1 * getHead() >> head 1 * getHead() >> head
_ * getApi(_) >> Mono.just(api) _ * getReader() >> api
} }
def status = new TrackBitcoinTx.TxStatus( def status = new TrackBitcoinTx.TxStatus(
txid, false, null, false, null, null, null, 0 txid, false, null, false, null, null, null, 0
@@ -210,13 +212,9 @@ class TrackBitcoinTxSpec extends Specification {
def "Check mempool until found"() { def "Check mempool until found"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
1 * getTx(txid) >> Mono.just([
txid: txid
])
}
Head head = Mock(Head) { Head head = Mock(Head) {
_ * getFlux() >> Flux.empty() _ * getFlux() >> Flux.empty()
} }
@@ -228,17 +226,20 @@ class TrackBitcoinTxSpec extends Specification {
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over
] ]
} }
BitcoinUpstream upstream = Mock(BitcoinUpstream) { BitcoinReader api = Mock(BitcoinReader) {
_ * getApi(_) >> Mono.just(api) 1 * getTx(txid) >> Mono.just([
txid: txid
])
_ * getMempool() >> mempoolAccess
}
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
_ * getHead() >> head _ * getHead() >> head
_ * getData() >> Mock(BitcoinReader) { _ * getReader() >> api
_ * getMempool() >> mempoolAccess
}
} }
when: when:
def steps = StepVerifier.withVirtualTime { def steps = StepVerifier.withVirtualTime {
track.untilFound(Chain.BITCOIN, api, upstream, txid).take(1) track.untilFound(Chain.BITCOIN, upstream, txid).take(1)
} }
then: then:
@@ -252,9 +253,9 @@ class TrackBitcoinTxSpec extends Specification {
def "Subscribe to an existing tx"() { def "Subscribe to an existing tx"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
DirectBitcoinApi api = Mock(DirectBitcoinApi) { BitcoinReader api = Mock(BitcoinReader) {
_ * getTx(txid) >> Mono.just([ _ * getTx(txid) >> Mono.just([
txid : txid, txid : txid,
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
@@ -267,18 +268,18 @@ class TrackBitcoinTxSpec extends Specification {
]) ])
} }
def next = Flux.fromIterable([10, 11, 12]).map { h -> 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) { Head head = Mock(Head) {
_ * getFlux() >> next _ * getFlux() >> next
} }
BitcoinUpstream upstream = Mock(BitcoinUpstream) { BitcoinMultistream upstream = Mock(BitcoinMultistream) {
_ * getApi(_) >> Mono.just(api) _ * getReader() >> api
_ * getHead() >> head _ * getHead() >> head
} }
when: when:
def act = track.subscribe(Chain.BITCOIN, api, upstream, txid) def act = track.subscribe(Chain.BITCOIN, upstream, txid)
then: then:
StepVerifier.create(act) StepVerifier.create(act)

View File

@@ -19,21 +19,12 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.test.UpstreamsMock import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.rpc.ReactorRpcClient
import io.infinitape.etherjar.rpc.RpcCall
import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.BlockJson
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.scheduler.Schedulers
import reactor.test.StepVerifier import reactor.test.StepVerifier
import spock.lang.Specification import spock.lang.Specification
@@ -64,9 +55,9 @@ class TrackEthereumAddressSpec extends Specification {
.setBalance("1234567890") .setBalance("1234567890")
.build() .build()
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams) TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
@@ -104,9 +95,9 @@ class TrackEthereumAddressSpec extends Specification {
return it return it
} }
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams) TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
@@ -121,6 +112,6 @@ class TrackEthereumAddressSpec extends Specification {
} }
.expectNext(exp2).as("Second block") .expectNext(exp2).as("Second block")
.thenCancel() .thenCancel()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(2))
} }
} }

View File

@@ -23,15 +23,13 @@ import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.test.UpstreamsMock import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId 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.BlockJson
import io.infinitape.etherjar.rpc.json.TransactionJson import io.infinitape.etherjar.rpc.json.TransactionJson
import io.infinitape.etherjar.rpc.json.TransactionRefJson import io.infinitape.etherjar.rpc.json.TransactionRefJson
@@ -98,9 +96,9 @@ class TrackEthereumTxSpec extends Specification {
.setTimestamp(blockJson.timestamp.toEpochMilli()) .setTimestamp(blockJson.timestamp.toEpochMilli())
).build() ).build()
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
apiMock.answer("eth_getTransactionByHash", [txId], txJson) apiMock.answer("eth_getTransactionByHash", [txId], txJson)
@@ -118,10 +116,10 @@ class TrackEthereumTxSpec extends Specification {
def "Wait for unknown transaction"() { def "Wait for unknown transaction"() {
setup: setup:
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
((AggregatedEthereumUpstreams) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) { ((EthereumMultistream) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) {
_ * getFlux() >> Flux.empty() _ * getFlux() >> Flux.empty()
} }
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
@@ -133,7 +131,7 @@ class TrackEthereumTxSpec extends Specification {
when: when:
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6) def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def act = StepVerifier.withVirtualTime( def act = StepVerifier.withVirtualTime(
{ trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).castApi(EthereumApi.class)) }, { trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).cast(EthereumMultistream)) },
{ scheduler }, { scheduler },
5) 5)
@@ -168,9 +166,9 @@ class TrackEthereumTxSpec extends Specification {
it it
} }
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
def scheduler = VirtualTimeScheduler.create(true) def scheduler = VirtualTimeScheduler.create(true)
trackTx.scheduler = scheduler trackTx.scheduler = scheduler
@@ -193,14 +191,14 @@ class TrackEthereumTxSpec extends Specification {
def "New block makes tx mined"() { def "New block makes tx mined"() {
setup: setup:
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6) def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def block = new BlockContainer( 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)] [TxId.from(txId)]
) )
@@ -215,14 +213,14 @@ class TrackEthereumTxSpec extends Specification {
def "New block without current tx requires a call"() { def "New block without current tx requires a call"() {
setup: setup:
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6) def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
def block = new BlockContainer( 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")] [TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")]
) )
apiMock.answer("eth_getTransactionByHash", [txId], null) apiMock.answer("eth_getTransactionByHash", [txId], null)
@@ -288,9 +286,9 @@ class TrackEthereumTxSpec extends Specification {
) )
def apiMock = TestingCommons.api(Stub(ReactorRpcClient)) def apiMock = TestingCommons.api()
def upstreamMock = TestingCommons.upstream(apiMock) def upstreamMock = TestingCommons.upstream(apiMock)
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams) TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
apiMock.answerOnce("eth_getTransactionByHash", [txId], null) apiMock.answerOnce("eth_getTransactionByHash", [txId], null)

View File

@@ -19,11 +19,10 @@ package io.emeraldpay.dshackle.test
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.grpc.Chain import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.grpc.stub.StreamObserver import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.rpc.ReactorRpcClient
import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.RpcResponseError
import io.infinitape.etherjar.rpc.json.ResponseJson import io.infinitape.etherjar.rpc.json.ResponseJson
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
@@ -31,16 +30,18 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.time.Duration
import java.util.concurrent.Callable import java.util.concurrent.Callable
class EthereumApiMock extends DirectEthereumApi { class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
private static final Logger log = LoggerFactory.getLogger(this) private static final Logger log = LoggerFactory.getLogger(this)
List<PredefinedResponse> predefined = [] List<PredefinedResponse> predefined = []
private ObjectMapper objectMapper private ObjectMapper objectMapper
EthereumApiMock(@NotNull ReactorRpcClient rpcClient, @NotNull ObjectMapper objectMapper, @NotNull Chain chain) { String id = "default"
super(rpcClient, null, objectMapper, new DirectCallMethods())
EthereumApiMock(@NotNull ObjectMapper objectMapper) {
this.objectMapper = objectMapper this.objectMapper = objectMapper
} }
@@ -55,10 +56,11 @@ class EthereumApiMock extends DirectEthereumApi {
} }
@Override @Override
Mono<byte[]> execute(int id, @NotNull String method, @NotNull List<?> params) { Mono<JsonRpcResponse> read(JsonRpcRequest request) {
Callable<byte[]> call = { Callable<JsonRpcResponse> call = {
def predefined = predefined.find { it.isSame(id, method, params) } def predefined = predefined.find { it.isSame(request.method, request.params) }
ResponseJson json = new ResponseJson<Object, Integer>(id: id) byte[] result = null
JsonRpcResponse.ResponseError error = null
if (predefined != null) { if (predefined != null) {
if (predefined.exception != null) { if (predefined.exception != null) {
predefined.onCalled() predefined.onCalled()
@@ -66,32 +68,37 @@ class EthereumApiMock extends DirectEthereumApi {
throw predefined.exception throw predefined.exception
} }
if (predefined.result instanceof RpcResponseError) { if (predefined.result instanceof RpcResponseError) {
json.error = predefined.result ((RpcResponseError) predefined.result).with { err ->
error = new JsonRpcResponse.ResponseError(err.code, err.message)
}
} else { } else {
json.result = predefined.result // ResponseJson json = new ResponseJson<Object, Integer>(id: 1, result: predefined.result)
result = objectMapper.writeValueAsBytes(predefined.result)
} }
predefined.onCalled() predefined.onCalled()
predefined.print() predefined.print()
} else { } else {
log.error("Method ${method} with ${params} is not mocked") log.error("Method ${request.method} with ${request.params} is not mocked")
json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked") error = new JsonRpcResponse.ResponseError(-32601, "Method ${request.method} with ${request.params} is not mocked")
} }
byte[] result = objectMapper.writeValueAsBytes(json) return new JsonRpcResponse(result, error)
return result } as Callable<JsonRpcResponse>
} as Callable<byte[]>
return Mono.fromCallable(call) return Mono.fromCallable(call)
} }
def nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) { def nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
request.itemsList.forEach { req -> request.itemsList.forEach { req ->
def resp = execute(req.id, req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray())) JsonRpcResponse resp = read(new JsonRpcRequest(req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray())))
resp.subscribe { .block(Duration.ofSeconds(5))
def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder() def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(req.id) .setId(req.id)
.setSucceed(true) .setSucceed(resp.hasResult())
.setPayload(ByteString.copyFrom(resp.block())) .setPayload(ByteString.copyFrom(resp.getResult()))
responseObserver.onNext(proto.build())
resp.error?.with { err ->
proto.setErrorMessage(err.message)
} }
responseObserver.onNext(proto.build())
} }
responseObserver.onCompleted() responseObserver.onCompleted()
} }
@@ -103,7 +110,7 @@ class EthereumApiMock extends DirectEthereumApi {
Integer limit Integer limit
Throwable exception Throwable exception
boolean isSame(int id, String method, List<?> params) { boolean isSame(String method, List<?> params) {
if (limit != null) { if (limit != null) {
if (limit <= 0) { if (limit <= 0) {
return false return false

View File

@@ -16,28 +16,20 @@
*/ */
package io.emeraldpay.dshackle.test package io.emeraldpay.dshackle.test
import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
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 reactor.core.publisher.Mono import reactor.core.publisher.Mono
class EthereumApiStub extends DirectEthereumApi { class EthereumApiStub implements Reader<JsonRpcRequest, JsonRpcResponse> {
private String id private String id
private static ObjectMapper objectMapper = TestingCommons.objectMapper()
private static ReactorRpcClient rpcClient = new RpcClientMock();
EthereumApiStub(Integer id) { EthereumApiStub(Integer id) {
this(id.toString()) this(id.toString())
} }
EthereumApiStub(String id) { EthereumApiStub(String id) {
super(rpcClient, null, objectMapper, new DirectCallMethods())
this.id = id this.id = id
} }
@@ -46,16 +38,9 @@ class EthereumApiStub extends DirectEthereumApi {
return "API Stub $id" return "API Stub $id"
} }
static class RpcClientMock implements ReactorRpcClient { @Override
Mono<JsonRpcResponse> read(JsonRpcRequest key) {
@Override return Mono.error(new Exception("Not implemented in mock"))
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"))
}
} }
} }

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import org.jetbrains.annotations.NotNull
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -28,9 +29,14 @@ class EthereumHeadMock implements Head {
private TopicProcessor<BlockContainer> bus = TopicProcessor.create() private TopicProcessor<BlockContainer> bus = TopicProcessor.create()
private Publisher<BlockContainer> predefined = null private Publisher<BlockContainer> predefined = null
private BlockContainer latest private BlockContainer latest
private List<Runnable> handlers = []
void nextBlock(BlockContainer block) { void nextBlock(BlockContainer block) {
handlers.forEach {
it.run()
}
assert block != null assert block != null
println("New block: ${block.height} / ${block.hash}")
latest = block latest = block
bus.onNext(block) bus.onNext(block)
} }
@@ -51,4 +57,9 @@ class EthereumHeadMock implements Head {
return Flux.concat(Mono.justOrEmpty(latest), bus).distinctUntilChanged() return Flux.concat(Mono.justOrEmpty(latest), bus).distinctUntilChanged()
} }
} }
@Override
void onBeforeBlock(@NotNull Runnable handler) {
handlers.add(handler)
}
} }

View File

@@ -16,18 +16,22 @@
*/ */
package io.emeraldpay.dshackle.test package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability 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.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.json.BlockJson
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
@@ -35,24 +39,33 @@ class EthereumUpstreamMock extends EthereumUpstream {
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock() EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) { static CallMethods allMethods() {
this(chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain)) new AggregatedCallMethods([
new DefaultEthereumMethods(TestingCommons.objectMapper(), Chain.ETHEREUM),
new DefaultBitcoinMethods(TestingCommons.objectMapper()),
new DirectCallMethods(["eth_test"])
])
} }
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) { EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(id, chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain)) this(chain, api, allMethods())
} }
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) { EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
this(id, chain, api, allMethods())
}
EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
this("test", chain, api, 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, super(id, chain, api, null,
UpstreamsConfig.Options.getDefaults(), new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()), UpstreamsConfig.Options.getDefaults(), new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
methods, TestingCommons.objectMapper()) methods, TestingCommons.objectMapper())
setLag(0) setLag(0)
setStatus(UpstreamAvailability.OK) setStatus(UpstreamAvailability.OK)
start()
} }
void nextBlock(BlockContainer block) { void nextBlock(BlockContainer block) {

View File

@@ -16,48 +16,60 @@
*/ */
package io.emeraldpay.dshackle.test 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.cache.Caches
import io.emeraldpay.dshackle.upstream.AggregatedUpstream import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
class UpstreamsMock implements Upstreams { class MultistreamHolderMock implements MultistreamHolder {
private Map<Chain, DefaultEthereumMethods> target = [:] private Map<Chain, DefaultEthereumMethods> target = [:]
private Map<Chain, AggregatedEthereumUpstreamsMock> upstreams = [:] private Map<Chain, Multistream> upstreams = [:]
UpstreamsMock(Chain chain, Upstream up) { MultistreamHolderMock(Chain chain, Upstream up) {
addUpstream(chain, up) addUpstream(chain, up)
} }
UpstreamsMock(Chain chain1, Upstream up1, Chain chain2, Upstream up2) {
addUpstream(chain1, up1)
addUpstream(chain2, up2)
}
AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull EthereumUpstream up) { Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
if (!upstreams.containsKey(chain)) { if (!upstreams.containsKey(chain)) {
upstreams[chain] = new AggregatedEthereumUpstreamsMock(chain, [up], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper()) if (BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM) {
upstreams[chain].start() if (up instanceof EthereumMultistream) {
upstreams[chain] = up
} else if (up instanceof EthereumUpstream) {
upstreams[chain] = new EthereumMultistreamMock(chain, [up as EthereumUpstream], Caches.default(TestingCommons.objectMapper()))
} else {
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
}
upstreams[chain].start()
} else if (BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN) {
if (up instanceof BitcoinMultistream) {
upstreams[chain] = up
} else if (up instanceof BitcoinUpstream) {
upstreams[chain] = new BitcoinMultistream(chain, [up as BitcoinUpstream], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper())
} else {
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
}
upstreams[chain].start()
}
} else { } else {
upstreams[chain].addUpstream(up) upstreams[chain].addUpstream(up)
} }
return upstreams[chain] return upstreams[chain]
} }
void setReader(@NotNull Chain chain, EthereumReader reader) {
upstreams[chain].customReader = reader
}
@Override @Override
AggregatedUpstream getUpstream(@NotNull Chain chain) { Multistream getUpstream(@NotNull Chain chain) {
return upstreams[chain] return upstreams[chain]
} }
@@ -85,12 +97,12 @@ class UpstreamsMock implements Upstreams {
return upstreams.containsKey(chain) return upstreams.containsKey(chain)
} }
static class AggregatedEthereumUpstreamsMock extends AggregatedEthereumUpstreams { static class EthereumMultistreamMock extends EthereumMultistream {
EthereumReader customReader = null EthereumReader customReader = null
AggregatedEthereumUpstreamsMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches, @NotNull ObjectMapper objectMapper) { EthereumMultistreamMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches) {
super(chain, upstreams, caches, objectMapper) super(chain, upstreams, caches, TestingCommons.objectMapper())
} }
@Override @Override

View File

@@ -1,6 +1,5 @@
/** /**
* Copyright (c) 2020 EmeraldPay, Inc * Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2020 ETCDEV GmbH
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * See the License for the specific language governing permissions and
* limitations under the License. * 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 import reactor.core.publisher.Mono
/** class ReaderMock<K, D> implements Reader<K, D> {
* A general interface to make a request to an Upstream API
*/
interface UpstreamApi {
/** private Map<K, D> mapping = new HashMap<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>
} 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.Caches
import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.config.CacheConfig import io.emeraldpay.dshackle.config.CacheConfig
import io.emeraldpay.dshackle.upstream.AggregatedUpstream import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream 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.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.JacksonRpcConverter
import io.infinitape.etherjar.rpc.ReactorRpcClient
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -50,32 +51,34 @@ class TestingCommons {
return objectMapper return objectMapper
} }
static EthereumApiMock api(ReactorRpcClient rpcClient) { static EthereumApiMock api() {
return new EthereumApiMock(rpcClient, objectMapper(), Chain.ETHEREUM) return new EthereumApiMock(objectMapper())
} }
static JacksonRpcConverter rpcConverter() { static JacksonRpcConverter rpcConverter() {
return new JacksonRpcConverter(objectMapper()) return new JacksonRpcConverter(objectMapper())
} }
static EthereumUpstreamMock upstream(DirectEthereumApi api) { static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
return new EthereumUpstreamMock(Chain.ETHEREUM, 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]) 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)) return new EthereumUpstreamMock(Chain.ETHEREUM, api, new DirectCallMethods(methods))
} }
static AggregatedUpstream aggregatedUpstream(DirectEthereumApi api) { static Multistream aggregatedUpstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
return aggregatedUpstream(upstream(api)) return aggregatedUpstream(upstream(api))
} }
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) { static Multistream aggregatedUpstream(EthereumUpstream up) {
return new AggregatedEthereumUpstreams(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper()) return new EthereumMultistream(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper()).tap {
start()
}
} }
static CachesFactory emptyCaches() { static CachesFactory emptyCaches() {

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
import java.time.Instant
class AbstractHeadSpec extends Specification {
def blocks = [1L, 2, 3, 4].collect { i ->
byte[] hash = new byte[32]
hash[0] = i as byte
new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, [])
}
def "Calls beforeBlock on each block"() {
setup:
TopicProcessor<BlockContainer> source = TopicProcessor.create()
def head = new TestHead()
def called = false
when:
head.follow(Flux.from(source))
head.onBeforeBlock {
called = true
}
def act = head.flux
source.onNext(blocks[0])
then:
StepVerifier.create(act)
.expectNext(blocks[0])
.then {
assert called
called = false
source.onNext(blocks[1])
}
.expectNext(blocks[1])
.then {
assert called
source.onComplete()
}
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Follows source"() {
setup:
TopicProcessor<BlockContainer> source = TopicProcessor.create()
def head = new TestHead()
when:
head.follow(Flux.from(source))
def act = head.flux
source.onNext(blocks[0])
then:
StepVerifier.create(act)
.expectNext(blocks[0])
.then { source.onNext(blocks[1]) }
.expectNext(blocks[1])
.then { source.onNext(blocks[2]) }
.expectNext(blocks[2])
.then { source.onNext(blocks[3]) }
.expectNext(blocks[3])
.then { source.onComplete() }
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Ignores block will less difficulty"() {
setup:
TopicProcessor<BlockContainer> source = TopicProcessor.create()
def head = new TestHead()
def wrongblock = new BlockContainer(
blocks[1].height, BlockId.from(blocks[1].hash.value.clone().tap { it[1] = 0xff as byte }),
blocks[1].difficulty - 1,
Instant.now(),
false, null, null, []
)
when:
head.follow(Flux.from(source))
def act = head.flux
source.onNext(blocks[0])
then:
StepVerifier.create(act)
.expectNext(blocks[0])
.then { source.onNext(blocks[1]) }
.expectNext(blocks[1])
.then { source.onNext(wrongblock) }
.then { source.onNext(blocks[3]) }
.expectNext(blocks[3])
.then { source.onComplete() }
.expectComplete()
.verify(Duration.ofSeconds(1))
}
class TestHead extends AbstractHead {
}
}

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