Merge pull request #6 from emeraldpay/feat/normal-request-flow
refactor request flow
This commit is contained in:
37
build.gradle
37
build.gradle
@@ -81,7 +81,6 @@ dependencies {
|
||||
implementation "io.infinitape:etherjar-hex:$etherjarVersion"
|
||||
implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion"
|
||||
implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion"
|
||||
implementation "io.infinitape:etherjar-rpc-emerald:$etherjarVersion"
|
||||
implementation "io.infinitape:etherjar-tx:$etherjarVersion"
|
||||
implementation 'org.bitcoinj:bitcoinj-core:0.15.8'
|
||||
|
||||
@@ -125,7 +124,7 @@ compileTestKotlin {
|
||||
|
||||
test {
|
||||
jvmArgs '-ea'
|
||||
testLogging.showStandardStreams = true
|
||||
testLogging.showStandardStreams = false
|
||||
testLogging.exceptionFormat = 'full'
|
||||
}
|
||||
|
||||
@@ -199,4 +198,38 @@ task generateVersion() {
|
||||
].join("\n")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Show the list of failed tests and output only for them, helpful for CI
|
||||
ext.failedTests = []
|
||||
tasks.withType(Test) {
|
||||
def stdout = new LinkedList<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 ""
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ class BlocksRedisCache(
|
||||
Instant.ofEpochMilli(meta.timestamp),
|
||||
false,
|
||||
value.value.toByteArray(),
|
||||
null,
|
||||
meta.txHashesList.map {
|
||||
TxId(it.toByteArray())
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.data.TxContainer
|
||||
import io.emeraldpay.dshackle.data.TxId
|
||||
import io.emeraldpay.dshackle.reader.CompoundReader
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumFullBlocksReader
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -158,11 +159,11 @@ open class Caches(
|
||||
}
|
||||
|
||||
fun getFullBlocks(): Reader<BlockId, BlockContainer> {
|
||||
return EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash)
|
||||
return EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash)
|
||||
}
|
||||
|
||||
fun getFullBlocksByHeight(): Reader<Long, BlockContainer> {
|
||||
return BlockByHeight(blocksByHeight, EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash))
|
||||
return BlockByHeight(blocksByHeight, EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash))
|
||||
}
|
||||
|
||||
enum class Tag {
|
||||
|
||||
@@ -59,7 +59,10 @@ class TxRedisCache(
|
||||
fun toProto(value: TxContainer): ByteArray {
|
||||
val meta = CachesProto.TxMeta.newBuilder()
|
||||
.setHash(ByteString.copyFrom(value.hash.value))
|
||||
.setHeight(value.height)
|
||||
|
||||
value.height?.let {
|
||||
meta.setHeight(it)
|
||||
}
|
||||
|
||||
value.blockId?.value?.let {
|
||||
meta.setBlockHash(ByteString.copyFrom(it))
|
||||
|
||||
@@ -86,19 +86,21 @@ class UpstreamsConfig {
|
||||
|
||||
open class UpstreamConnection
|
||||
|
||||
open class RpcConnection : UpstreamConnection() {
|
||||
var rpc: HttpEndpoint? = null
|
||||
}
|
||||
|
||||
class GrpcConnection : UpstreamConnection() {
|
||||
var host: String? = null
|
||||
var port: Int = 0
|
||||
var auth: AuthConfig.ClientTlsAuth? = null
|
||||
}
|
||||
|
||||
class EthereumConnection : UpstreamConnection() {
|
||||
var rpc: HttpEndpoint? = null
|
||||
class EthereumConnection : RpcConnection() {
|
||||
var ws: WsEndpoint? = null
|
||||
}
|
||||
|
||||
class BitcoinConnection : UpstreamConnection() {
|
||||
var rpc: HttpEndpoint? = null
|
||||
class BitcoinConnection : RpcConnection() {
|
||||
}
|
||||
|
||||
class HttpEndpoint(val url: URI) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import java.math.BigInteger
|
||||
import java.time.Instant
|
||||
|
||||
@@ -30,12 +31,13 @@ class BlockContainer(
|
||||
val timestamp: Instant,
|
||||
val full: Boolean,
|
||||
json: ByteArray?,
|
||||
val parsed: Any?,
|
||||
val transactions: List<TxId> = emptyList()
|
||||
) : SourceContainer(json) {
|
||||
) : SourceContainer(json, parsed) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer {
|
||||
fun from(block: BlockJson<*>, raw: ByteArray): BlockContainer {
|
||||
val hasTransactions = block.transactions?.filterIsInstance<TransactionJson>()?.count() ?: 0 > 0
|
||||
return BlockContainer(
|
||||
block.number,
|
||||
@@ -43,10 +45,26 @@ class BlockContainer(
|
||||
block.totalDifficulty,
|
||||
block.timestamp,
|
||||
hasTransactions,
|
||||
objectMapper.writeValueAsBytes(block),
|
||||
raw,
|
||||
block,
|
||||
block.transactions?.map { TxId.from(it.hash) } ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer {
|
||||
return from(block, objectMapper.writeValueAsBytes(block))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(raw: ByteArray, objectMapper: ObjectMapper): BlockContainer {
|
||||
val block = objectMapper.readValue(raw, BlockJson::class.java)
|
||||
return from(block, raw)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Block $height = $hash"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -25,6 +25,11 @@ class BlockId(
|
||||
) : HashId(value) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(hash: ByteArray): BlockId {
|
||||
return BlockId(hash)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(hash: BlockHash): BlockId {
|
||||
return BlockId(hash.bytes)
|
||||
|
||||
@@ -40,6 +40,10 @@ open class HashId(
|
||||
return String(hex)
|
||||
}
|
||||
|
||||
fun toHexWithPrefix(): String {
|
||||
return "0x" + toHex()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is HashId) return false
|
||||
|
||||
@@ -16,10 +16,24 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.data
|
||||
|
||||
import java.lang.ClassCastException
|
||||
|
||||
abstract class SourceContainer(
|
||||
val json: ByteArray?
|
||||
val json: ByteArray?,
|
||||
private val parsed: Any?
|
||||
) {
|
||||
|
||||
fun <T> getParsed(clazz: Class<T>): T? {
|
||||
if (parsed == null) {
|
||||
return null
|
||||
}
|
||||
if (clazz.isAssignableFrom(parsed.javaClass)) {
|
||||
return parsed as T
|
||||
}
|
||||
throw ClassCastException("Cannot cast ${parsed.javaClass} to $clazz")
|
||||
}
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is SourceContainer) return false
|
||||
|
||||
@@ -20,20 +20,26 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||
|
||||
class TxContainer(
|
||||
val height: Long,
|
||||
val height: Long?,
|
||||
val hash: TxId,
|
||||
val blockId: BlockId?,
|
||||
json: ByteArray?
|
||||
) : SourceContainer(json) {
|
||||
json: ByteArray?,
|
||||
parsed: Any? = null
|
||||
) : SourceContainer(json, parsed) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(tx: TransactionJson, objectMapper: ObjectMapper): TxContainer {
|
||||
return from(tx, objectMapper.writeValueAsBytes(tx))
|
||||
}
|
||||
|
||||
fun from(tx: TransactionJson, raw: ByteArray): TxContainer {
|
||||
return TxContainer(
|
||||
tx.blockNumber,
|
||||
TxId.from(tx.hash),
|
||||
BlockId.from(tx.blockHash),
|
||||
objectMapper.writeValueAsBytes(tx)
|
||||
tx.blockHash?.let { BlockId.from(it) },
|
||||
raw,
|
||||
tx
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,17 @@ open class AlwaysQuorum: CallQuorum {
|
||||
return resolved
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
|
||||
override fun isFailed(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||
result = response
|
||||
resolved = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
||||
override fun record(error: RpcException, upstream: Upstream) {
|
||||
}
|
||||
|
||||
override fun getResult(): ByteArray? {
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||
|
||||
open class BroadcastQuorum(
|
||||
jacksonRpcConverter: JacksonRpcConverter,
|
||||
objectMapper: ObjectMapper,
|
||||
val quorum: Int = 3
|
||||
): CallQuorum, ValueAwareQuorum<String>(jacksonRpcConverter, String::class.java) {
|
||||
) : CallQuorum, ValueAwareQuorum<String>(objectMapper, String::class.java) {
|
||||
|
||||
private var result: ByteArray? = null
|
||||
private var txid: String? = null
|
||||
@@ -36,11 +37,15 @@ open class BroadcastQuorum(
|
||||
return calls >= quorum
|
||||
}
|
||||
|
||||
override fun isFailed(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getResult(): ByteArray? {
|
||||
return result
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) {
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
||||
calls++
|
||||
if (txid == null && responseValue != null) {
|
||||
txid = responseValue
|
||||
@@ -48,7 +53,7 @@ open class BroadcastQuorum(
|
||||
}
|
||||
}
|
||||
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) {
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
|
||||
calls++
|
||||
if (result == null) {
|
||||
|
||||
@@ -31,8 +31,10 @@ interface CallQuorum {
|
||||
fun init(head: Head)
|
||||
|
||||
fun isResolved(): Boolean
|
||||
fun record(response: ByteArray, upstream: Upstream<*>): Boolean
|
||||
fun record(error: RpcException, upstream: Upstream<*>)
|
||||
fun isFailed(): Boolean
|
||||
|
||||
fun record(response: ByteArray, upstream: Upstream): Boolean
|
||||
fun record(error: RpcException, upstream: Upstream)
|
||||
fun getResult(): ByteArray?
|
||||
|
||||
companion object {
|
||||
@@ -42,8 +44,8 @@ interface CallQuorum {
|
||||
}
|
||||
}
|
||||
|
||||
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*>>, CallQuorum> {
|
||||
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*>>, CallQuorum> { a, b ->
|
||||
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> {
|
||||
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { a, b ->
|
||||
a.record(b.t1, b.t2)
|
||||
return@BiFunction a
|
||||
}
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
|
||||
open class NonEmptyQuorum(
|
||||
jacksonRpcConverter: JacksonRpcConverter,
|
||||
objectMapper: ObjectMapper,
|
||||
val maxTries: Int = 3
|
||||
): CallQuorum, ValueAwareQuorum<Any>(jacksonRpcConverter, Any::class.java) {
|
||||
) : CallQuorum, ValueAwareQuorum<Any>(objectMapper, Any::class.java) {
|
||||
|
||||
private var result: ByteArray? = null
|
||||
private var tries: Int = 0
|
||||
@@ -33,10 +34,14 @@ open class NonEmptyQuorum(
|
||||
}
|
||||
|
||||
override fun isResolved(): Boolean {
|
||||
return result != null || tries >= maxTries
|
||||
return result != null
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*>) {
|
||||
override fun isFailed(): Boolean {
|
||||
return tries >= maxTries
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
|
||||
tries++
|
||||
if (responseValue != null) {
|
||||
result = response
|
||||
@@ -47,10 +52,12 @@ open class NonEmptyQuorum(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) {
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||
tries++
|
||||
}
|
||||
|
||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
||||
override fun record(error: RpcException, upstream: Upstream) {
|
||||
tries++
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.infinitape.etherjar.hex.HexQuantity
|
||||
@@ -25,9 +26,9 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
open class NonceQuorum(
|
||||
jacksonRpcConverter: JacksonRpcConverter,
|
||||
objectMapper: ObjectMapper,
|
||||
val tries: Int = 3
|
||||
): CallQuorum, ValueAwareQuorum<String>(jacksonRpcConverter, String::class.java) {
|
||||
) : CallQuorum, ValueAwareQuorum<String>(objectMapper, String::class.java) {
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
private var resultValue = 0L
|
||||
@@ -40,11 +41,15 @@ open class NonceQuorum(
|
||||
|
||||
override fun isResolved(): Boolean {
|
||||
lock.withLock {
|
||||
return receivedTimes >= tries || errors >= tries
|
||||
return receivedTimes >= tries && !isFailed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) {
|
||||
override fun isFailed(): Boolean {
|
||||
return errors >= tries
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
||||
val value = responseValue?.let { str ->
|
||||
HexQuantity.from(str).value.toLong()
|
||||
}
|
||||
@@ -63,11 +68,7 @@ open class NonceQuorum(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) {
|
||||
errors++
|
||||
}
|
||||
|
||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||
errors++
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,11 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
||||
return result.get() != null
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
|
||||
override fun isFailed(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||
val lagging = upstream.getLag() > maxLag
|
||||
if (!lagging) {
|
||||
result.set(response)
|
||||
@@ -41,10 +45,9 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
||||
override fun record(error: RpcException, upstream: Upstream) {
|
||||
}
|
||||
|
||||
|
||||
override fun getResult(): ByteArray {
|
||||
return result.get()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt
Normal file
105
src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt
Normal 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
|
||||
)
|
||||
}
|
||||
@@ -16,23 +16,24 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
abstract class ValueAwareQuorum<T>(
|
||||
val jacksonRpcConverter: JacksonRpcConverter,
|
||||
val objectMapper: ObjectMapper,
|
||||
val clazz: Class<T>
|
||||
): CallQuorum {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java)
|
||||
|
||||
fun extractValue(response: ByteArray, clazz: Class<T>): T? {
|
||||
return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
|
||||
return objectMapper.readValue(response.inputStream(), clazz)
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
|
||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||
try {
|
||||
val value = extractValue(response, clazz)
|
||||
recordValue(response, value, upstream)
|
||||
@@ -44,12 +45,12 @@ abstract class ValueAwareQuorum<T>(
|
||||
return isResolved();
|
||||
}
|
||||
|
||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
||||
override fun record(error: RpcException, upstream: Upstream) {
|
||||
recordError(null, error.rpcMessage, upstream)
|
||||
}
|
||||
|
||||
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream<*>)
|
||||
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
|
||||
|
||||
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>)
|
||||
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream)
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.reader
|
||||
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
interface Reader<K, D> {
|
||||
interface Reader<in K, D> {
|
||||
|
||||
fun read(key: K): Mono<D>
|
||||
|
||||
|
||||
@@ -29,15 +29,15 @@ import reactor.core.publisher.Mono
|
||||
|
||||
@Service
|
||||
class Describe(
|
||||
@Autowired private val upstreams: Upstreams,
|
||||
@Autowired private val multistreamHolder: MultistreamHolder,
|
||||
@Autowired private val subscribeStatus: SubscribeStatus
|
||||
) {
|
||||
|
||||
fun describe(requestMono: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
|
||||
return requestMono.map { _ ->
|
||||
val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
|
||||
upstreams.getAvailable().forEach { chain ->
|
||||
upstreams.getUpstream(chain)?.let { chainUpstreams ->
|
||||
multistreamHolder.getAvailable().forEach { chain ->
|
||||
multistreamHolder.getUpstream(chain)?.let { chainUpstreams ->
|
||||
val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll())
|
||||
val targets = chainUpstreams.getMethods().getSupportedMethods()
|
||||
val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder()
|
||||
|
||||
@@ -24,8 +24,14 @@ import io.emeraldpay.dshackle.SilentException
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
|
||||
import io.emeraldpay.dshackle.quorum.CallQuorum
|
||||
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
|
||||
import io.emeraldpay.dshackle.quorum.QuorumRpcReader
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -36,21 +42,23 @@ import java.lang.Exception
|
||||
|
||||
@Service
|
||||
open class NativeCall(
|
||||
@Autowired private val upstreams: Upstreams,
|
||||
@Autowired private val multistreamHolder: MultistreamHolder,
|
||||
@Autowired private val objectMapper: ObjectMapper
|
||||
) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(NativeCall::class.java)
|
||||
|
||||
var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default()
|
||||
|
||||
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
|
||||
return requestMono.flatMapMany(this::prepareCall)
|
||||
.map(this::setupCallParams)
|
||||
.parallel()
|
||||
.flatMap(this::fetch)
|
||||
.sequential()
|
||||
.map(this::buildResponse)
|
||||
.doOnError { e -> log.warn("Error during native call: ${e.message}") }
|
||||
.onErrorResume(this::processException)
|
||||
.flatMap(this::fetch)
|
||||
.sequential()
|
||||
.map(this::buildResponse)
|
||||
.doOnError { e -> log.warn("Error during native call: ${e.message}") }
|
||||
.onErrorResume(this::processException)
|
||||
}
|
||||
|
||||
fun setupCallParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> {
|
||||
@@ -87,17 +95,17 @@ open class NativeCall(
|
||||
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
||||
}
|
||||
|
||||
if (!upstreams.isAvailable(chain)) {
|
||||
if (!multistreamHolder.isAvailable(chain)) {
|
||||
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
||||
}
|
||||
|
||||
val upstream = upstreams.getUpstream(chain)
|
||||
val upstream = multistreamHolder.getUpstream(chain)
|
||||
?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain)))
|
||||
|
||||
return prepareCall(request, upstream)
|
||||
}
|
||||
|
||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<*>): Flux<CallContext<RawCallDetails>> {
|
||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: Multistream): Flux<CallContext<RawCallDetails>> {
|
||||
return request.itemsList.toFlux().map {
|
||||
val method = it.method
|
||||
val params = it.payload.toStringUtf8()
|
||||
@@ -115,75 +123,29 @@ open class NativeCall(
|
||||
}
|
||||
|
||||
fun fetch(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||
return fetchFromCache(ctx)
|
||||
.onErrorResume { t ->
|
||||
log.warn("Failed to read from cache", t);
|
||||
Mono.empty()
|
||||
}
|
||||
.switchIfEmpty(
|
||||
return ctx.upstream.getRoutedApi(ctx.matcher)
|
||||
.flatMap { api ->
|
||||
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map {
|
||||
ctx.withPayload(it)
|
||||
}
|
||||
}.switchIfEmpty(
|
||||
Mono.just(ctx).flatMap(this::executeOnRemote)
|
||||
)
|
||||
}
|
||||
|
||||
fun fetchFromCache(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||
val cachingApi = ctx.upstream.cache
|
||||
return cachingApi.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { ctx.withPayload(it) }
|
||||
.onErrorMap {
|
||||
CallFailure(ctx.id, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||
val apis = ctx.getApis()
|
||||
apis.request(1)
|
||||
var failures = 0
|
||||
return Flux.from(apis)
|
||||
.flatMap { api ->
|
||||
val upstream = ctx.upstream
|
||||
api.execute(ctx.id, ctx.payload.method, ctx.payload.params)
|
||||
// on error notify quorum, it may use error message or other details
|
||||
.doOnError { err ->
|
||||
if (err is RpcException) {
|
||||
ctx.callQuorum.record(err, upstream)
|
||||
}
|
||||
}
|
||||
.map { Tuples.of(it, upstream) }
|
||||
}
|
||||
.retry {
|
||||
failures++
|
||||
if (ctx.callQuorum.isResolved()) {
|
||||
false
|
||||
} else if (failures < 3) {
|
||||
apis.request(1)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
// record all correct responses until quorum reached
|
||||
.reduce(ctx.callQuorum, {res, a ->
|
||||
if (res.record(a.t1, a.t2)) {
|
||||
apis.resolve()
|
||||
} else {
|
||||
apis.request(1)
|
||||
}
|
||||
res
|
||||
})
|
||||
// if last call resulted in error it's still possible that request was resolved correctly. i.e. for BroadcastQuorum
|
||||
.onErrorResume { err ->
|
||||
if (ctx.callQuorum.isResolved()) {
|
||||
Mono.just(ctx.callQuorum)
|
||||
} else {
|
||||
Mono.error(err)
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
if (!it.isResolved()) {
|
||||
log.debug("No quorum for ${ctx.payload.method} as ${ctx.callQuorum}")
|
||||
}
|
||||
}
|
||||
.filter { it.isResolved() }
|
||||
if (!ctx.upstream.getMethods().isAllowed(ctx.payload.method)) {
|
||||
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
|
||||
}
|
||||
val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum)
|
||||
return reader.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
|
||||
.map {
|
||||
val result = it.getResult()
|
||||
?: throw CallFailure(ctx.id, Exception("No response from upstream for ${ctx.payload.method}"))
|
||||
ctx.withPayload(result)
|
||||
ctx.withPayload(it.value)
|
||||
}
|
||||
.onErrorMap {
|
||||
log.error("Failed to make a call", it)
|
||||
@@ -204,7 +166,7 @@ open class NativeCall(
|
||||
}
|
||||
|
||||
open class CallContext<T>(val id: Int,
|
||||
val upstream: AggregatedUpstream<*>,
|
||||
val upstream: Multistream,
|
||||
val matcher: Selector.Matcher,
|
||||
val callQuorum: CallQuorum,
|
||||
val payload: T) {
|
||||
@@ -212,8 +174,8 @@ open class NativeCall(
|
||||
return CallContext(id, upstream, matcher, callQuorum, payload)
|
||||
}
|
||||
|
||||
fun getApis(): ApiSource<*> {
|
||||
return upstream.getApis(matcher)
|
||||
fun getApis(): ApiSource {
|
||||
return upstream.getApiSource(matcher)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -33,7 +31,7 @@ import reactor.core.publisher.Mono
|
||||
|
||||
@Service
|
||||
class StreamHead(
|
||||
@Autowired private val upstreams: Upstreams
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(StreamHead::class.java)
|
||||
@@ -42,7 +40,7 @@ class StreamHead(
|
||||
return requestMono.map { request ->
|
||||
Chain.byId(request.type.number)
|
||||
}.flatMapMany { chain ->
|
||||
val up = upstreams.getUpstream(chain)
|
||||
val up = multistreamHolder.getUpstream(chain)
|
||||
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
|
||||
up.getHead()
|
||||
.getFlux()
|
||||
@@ -64,7 +62,7 @@ class StreamHead(
|
||||
return BlockchainOuterClass.ChainHead.newBuilder()
|
||||
.setChainValue(chain.id)
|
||||
.setHeight(block.height)
|
||||
.setTimestamp(block.timestamp!!.toEpochMilli())
|
||||
.setTimestamp(block.timestamp.toEpochMilli())
|
||||
.setWeight(ByteString.copyFrom(block.difficulty.toByteArray()))
|
||||
.setBlockId(block.hash.toHex())
|
||||
.build()
|
||||
|
||||
@@ -27,13 +27,13 @@ import reactor.core.publisher.Mono
|
||||
|
||||
@Service
|
||||
class SubscribeStatus(
|
||||
@Autowired private val upstreams: Upstreams
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
) {
|
||||
|
||||
fun subscribeStatus(requestMono: Mono<BlockchainOuterClass.StatusRequest>): Flux<BlockchainOuterClass.ChainStatus> {
|
||||
return requestMono.flatMapMany {
|
||||
val ups = upstreams.getAvailable().mapNotNull { chain ->
|
||||
val chainUpstream = upstreams.getUpstream(chain)
|
||||
val ups = multistreamHolder.getAvailable().mapNotNull { chain ->
|
||||
val chainUpstream = multistreamHolder.getUpstream(chain)
|
||||
chainUpstream?.observeStatus()?.map { avail ->
|
||||
ChainSubscription(chain, chainUpstream, avail)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class SubscribeStatus(
|
||||
}
|
||||
}
|
||||
|
||||
fun chainStatus(chain: Chain, ups: List<Upstream<*>>): BlockchainOuterClass.ChainStatus {
|
||||
fun chainStatus(chain: Chain, ups: List<Upstream>): BlockchainOuterClass.ChainStatus {
|
||||
val available = ups.map { u ->
|
||||
u.getStatus()
|
||||
}.min() ?: UpstreamAvailability.UNAVAILABLE
|
||||
@@ -60,6 +60,6 @@ class SubscribeStatus(
|
||||
.build()
|
||||
}
|
||||
|
||||
class ChainSubscription(val chain: Chain, val up: AggregatedUpstream<*>, val avail: UpstreamAvailability)
|
||||
class ChainSubscription(val chain: Chain, val up: Multistream, val avail: UpstreamAvailability)
|
||||
|
||||
}
|
||||
@@ -19,9 +19,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -35,7 +34,7 @@ import kotlin.collections.HashMap
|
||||
|
||||
@Service
|
||||
class TrackBitcoinAddress(
|
||||
@Autowired private val upstreams: Upstreams
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
) : TrackAddress {
|
||||
|
||||
companion object {
|
||||
@@ -43,7 +42,7 @@ class TrackBitcoinAddress(
|
||||
}
|
||||
|
||||
override fun isSupported(chain: Chain): Boolean {
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain)
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
|
||||
}
|
||||
|
||||
fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? {
|
||||
@@ -63,8 +62,8 @@ class TrackBitcoinAddress(
|
||||
}
|
||||
}
|
||||
|
||||
fun requestBalances(chain: Chain, api: DirectBitcoinApi, addresses: List<String>): Flux<AddressBalance> {
|
||||
return api.executeAndResult(0, "listunspent", emptyList(), List::class.java)
|
||||
fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: List<String>): Flux<AddressBalance> {
|
||||
return api.getReader().listUnspent()
|
||||
.flatMapMany { unspents ->
|
||||
val result = getTotal(chain, addresses, unspents)
|
||||
Flux.fromIterable(result)
|
||||
@@ -73,17 +72,14 @@ class TrackBitcoinAddress(
|
||||
|
||||
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||
val chain = Chain.byId(request.asset.chainValue)
|
||||
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
|
||||
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
|
||||
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
|
||||
if (addresses.isEmpty()) {
|
||||
return Flux.empty()
|
||||
}
|
||||
val result = upstream.getApi(Selector.empty).flatMapMany { api ->
|
||||
requestBalances(chain, api, addresses)
|
||||
.map(this@TrackBitcoinAddress::buildResponse)
|
||||
}
|
||||
return result
|
||||
return requestBalances(chain, upstream, addresses)
|
||||
.map(this@TrackBitcoinAddress::buildResponse)
|
||||
}
|
||||
|
||||
fun getTotal(chain: Chain, addresses: List<String>, unspents: List<*>): List<AddressBalance> {
|
||||
@@ -122,20 +118,16 @@ class TrackBitcoinAddress(
|
||||
|
||||
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||
val chain = Chain.byId(request.asset.chainValue)
|
||||
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
|
||||
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
|
||||
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
|
||||
if (addresses.isEmpty()) {
|
||||
return Flux.empty()
|
||||
}
|
||||
val initial = upstream.getApi(Selector.empty).flatMapMany { api ->
|
||||
requestBalances(chain, api, addresses)
|
||||
}
|
||||
val initial = requestBalances(chain, upstream, addresses)
|
||||
val following = upstream.getHead().getFlux()
|
||||
.flatMap { block ->
|
||||
upstream.getApi(Selector.empty).flatMapMany { api ->
|
||||
requestBalances(chain, api, addresses)
|
||||
}
|
||||
requestBalances(chain, upstream, addresses)
|
||||
}
|
||||
val last = HashMap<Address, BigInteger>()
|
||||
val result = Flux.merge(initial, following)
|
||||
|
||||
@@ -20,10 +20,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -39,7 +37,7 @@ import kotlin.math.min
|
||||
|
||||
@Service
|
||||
class TrackBitcoinTx(
|
||||
@Autowired private val upstreams: Upstreams
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
) : TrackTx {
|
||||
|
||||
companion object {
|
||||
@@ -47,36 +45,35 @@ class TrackBitcoinTx(
|
||||
}
|
||||
|
||||
override fun isSupported(chain: Chain): Boolean {
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain)
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
|
||||
}
|
||||
|
||||
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
||||
val chain = Chain.byId(request.chainValue)
|
||||
val upstream = upstreams.getUpstream(chain)?.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
|
||||
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
|
||||
?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
|
||||
val txid = request.txId
|
||||
val confirmations = max(min(1, request.confirmationLimit), 12)
|
||||
return upstream.getApi(Selector.empty).flatMapMany { api ->
|
||||
subscribe(chain, api, upstream, txid)
|
||||
}.takeUntil { tx ->
|
||||
tx.confirmations >= confirmations
|
||||
}.map(this::asProto)
|
||||
return subscribe(chain, upstream, txid)
|
||||
.takeUntil { tx ->
|
||||
tx.confirmations >= confirmations
|
||||
}.map(this::asProto)
|
||||
}
|
||||
|
||||
fun subscribe(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> {
|
||||
return loadExisting(api, txid)
|
||||
fun subscribe(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux<TxStatus> {
|
||||
return loadExisting(upstream, txid)
|
||||
.flatMapMany { status ->
|
||||
if (status.mined) {
|
||||
//Head almost always knows the current height, so it can continue with calculating confirmations
|
||||
//without publishing an empty TxStatus first
|
||||
continueWithMined(api, upstream, status)
|
||||
continueWithMined(upstream, status)
|
||||
} else {
|
||||
loadMempool(upstream, txid)
|
||||
.flatMapMany { tx ->
|
||||
val next = if (tx.found) {
|
||||
untilMined(upstream, tx)
|
||||
} else {
|
||||
untilFound(chain, api, upstream, txid)
|
||||
untilFound(chain, upstream, txid)
|
||||
}
|
||||
//fist provide the current status, then updates
|
||||
Flux.concat(Mono.just(tx), next)
|
||||
@@ -85,8 +82,8 @@ class TrackBitcoinTx(
|
||||
}
|
||||
}
|
||||
|
||||
fun continueWithMined(api: DirectBitcoinApi, upstream: BitcoinUpstream, status: TxStatus): Flux<TxStatus> {
|
||||
return api.getBlock(status.blockHash!!)
|
||||
fun continueWithMined(upstream: BitcoinMultistream, status: TxStatus): Flux<TxStatus> {
|
||||
return upstream.getReader().getBlock(status.blockHash!!)
|
||||
.map { block ->
|
||||
TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block))
|
||||
}.flatMapMany { tx ->
|
||||
@@ -94,41 +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))
|
||||
.take(Duration.ofMinutes(10))
|
||||
.flatMap { loadMempool(upstream, txid) }
|
||||
.skipUntil { it.found }
|
||||
.flatMap { subscribe(chain, api, upstream, txid) }
|
||||
.flatMap { subscribe(chain, upstream, txid) }
|
||||
.doOnError { t ->
|
||||
log.error("Failed to wait until found", t)
|
||||
}
|
||||
}
|
||||
|
||||
fun untilMined(upstream: BitcoinUpstream, tx: TxStatus): Mono<TxStatus> {
|
||||
fun untilMined(upstream: BitcoinMultistream, tx: TxStatus): Mono<TxStatus> {
|
||||
return upstream.getHead().getFlux().flatMap {
|
||||
upstream.getApi(Selector.empty).flatMap { api ->
|
||||
loadExisting(api, tx.txid)
|
||||
}.filter { it.mined }
|
||||
loadExisting(upstream, tx.txid)
|
||||
.filter { it.mined }
|
||||
}.single()
|
||||
}
|
||||
|
||||
fun withConfirmations(upstream: BitcoinUpstream, tx: TxStatus): Flux<TxStatus> {
|
||||
fun withConfirmations(upstream: BitcoinMultistream, tx: TxStatus): Flux<TxStatus> {
|
||||
return upstream.getHead().getFlux().map {
|
||||
tx.withHead(it.height)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadExisting(api: DirectBitcoinApi, txid: String): Mono<TxStatus> {
|
||||
val mined = api.getTx(txid)
|
||||
fun loadExisting(api: BitcoinMultistream, txid: String): Mono<TxStatus> {
|
||||
val mined = api.getReader().getTx(txid)
|
||||
return mined.map {
|
||||
val block = it["blockhash"] as String?
|
||||
TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it))
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMempool(upstream: BitcoinUpstream, txid: String): Mono<TxStatus> {
|
||||
val mempool = upstream.getData().getMempool().get()
|
||||
fun loadMempool(upstream: BitcoinMultistream, txid: String): Mono<TxStatus> {
|
||||
val mempool = upstream.getReader().getMempool().get()
|
||||
return mempool.map {
|
||||
if (it.contains(txid)) {
|
||||
TxStatus(txid, found = true, mined = false)
|
||||
|
||||
@@ -21,9 +21,8 @@ import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.domain.Address
|
||||
import io.infinitape.etherjar.domain.Wei
|
||||
@@ -35,13 +34,13 @@ import reactor.core.publisher.Mono
|
||||
|
||||
@Service
|
||||
class TrackEthereumAddress(
|
||||
@Autowired private val upstreams: Upstreams
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
) : TrackAddress {
|
||||
|
||||
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
|
||||
|
||||
override fun isSupported(chain: Chain): Boolean {
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain)
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
|
||||
}
|
||||
|
||||
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||
@@ -52,7 +51,7 @@ class TrackEthereumAddress(
|
||||
|
||||
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||
val chain = Chain.byId(request.asset.chainValue)
|
||||
val head = upstreams.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
|
||||
val head = multistreamHolder.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
|
||||
val balances = initAddress(request)
|
||||
.flatMap { tracked ->
|
||||
val current = getBalance(tracked)
|
||||
@@ -87,14 +86,14 @@ class TrackEthereumAddress(
|
||||
}
|
||||
}
|
||||
|
||||
fun getUpstream(chain: Chain): AggregatedEthereumUpstreams {
|
||||
return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java)
|
||||
fun getUpstream(chain: Chain): EthereumMultistream {
|
||||
return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java)
|
||||
?: throw SilentException.UnsupportedBlockchain(chain)
|
||||
}
|
||||
|
||||
private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux<TrackedAddress> {
|
||||
val chain = Chain.byId(request.asset.chainValue)
|
||||
if (!upstreams.isAvailable(chain)) {
|
||||
if (!multistreamHolder.isAvailable(chain)) {
|
||||
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||
}
|
||||
if (request.asset.code?.toLowerCase() != "ether") {
|
||||
|
||||
@@ -23,10 +23,8 @@ import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.data.TxId
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
@@ -50,7 +48,7 @@ import kotlin.math.min
|
||||
|
||||
@Service
|
||||
class TrackEthereumTx(
|
||||
@Autowired private val upstreams: Upstreams
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
) : TrackTx {
|
||||
|
||||
companion object {
|
||||
@@ -65,7 +63,7 @@ class TrackEthereumTx(
|
||||
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
|
||||
|
||||
override fun isSupported(chain: Chain): Boolean {
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain)
|
||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
|
||||
}
|
||||
|
||||
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
||||
@@ -85,12 +83,12 @@ class TrackEthereumTx(
|
||||
}
|
||||
|
||||
|
||||
fun getUpstream(chain: Chain): AggregatedEthereumUpstreams {
|
||||
return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java)
|
||||
fun getUpstream(chain: Chain): EthereumMultistream {
|
||||
return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java)
|
||||
?: throw SilentException.UnsupportedBlockchain(chain)
|
||||
}
|
||||
|
||||
fun subscribe(base: TxDetails, up: AggregatedEthereumUpstreams): Flux<TxDetails> {
|
||||
fun subscribe(base: TxDetails, up: EthereumMultistream): Flux<TxDetails> {
|
||||
var latestTx = base
|
||||
|
||||
val untilFound = Mono.just(latestTx)
|
||||
@@ -215,7 +213,7 @@ class TrackEthereumTx(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFromBlock(upstream: AggregatedEthereumUpstreams, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> {
|
||||
fun updateFromBlock(upstream: EthereumMultistream, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> {
|
||||
return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) {
|
||||
val updated = tx.withStatus(
|
||||
blockHash = blockTx.blockHash,
|
||||
|
||||
@@ -19,19 +19,20 @@ package io.emeraldpay.dshackle.startup
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.FileResolver
|
||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.upstream.CurrentUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory
|
||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcHttpClient
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Repository
|
||||
@@ -44,9 +45,10 @@ import kotlin.collections.HashMap
|
||||
@Repository
|
||||
open class ConfiguredUpstreams(
|
||||
@Autowired private val objectMapper: ObjectMapper,
|
||||
@Autowired private val currentUpstreams: CurrentUpstreams,
|
||||
@Autowired private val currentUpstreams: CurrentMultistreamHolder,
|
||||
@Autowired private val fileResolver: FileResolver,
|
||||
@Autowired private val config: UpstreamsConfig
|
||||
@Autowired private val config: UpstreamsConfig,
|
||||
@Autowired private val cachesFactory: CachesFactory
|
||||
) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
|
||||
@@ -134,80 +136,61 @@ open class ConfiguredUpstreams(
|
||||
options: UpstreamsConfig.Options) {
|
||||
|
||||
val conn = config.connection!!
|
||||
var rpcApi: DirectBitcoinApi? = null
|
||||
val directApi: Reader<JsonRpcRequest, JsonRpcResponse>? = buildHttpClient(config)
|
||||
if (directApi == null) {
|
||||
log.warn("Upstream doesn't have API configuration")
|
||||
return
|
||||
}
|
||||
|
||||
val methods = buildMethods(config, chain)
|
||||
conn.rpc?.let { endpoint ->
|
||||
val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!)
|
||||
rpcApi = DirectBitcoinApi(rpcClient, objectMapper, methods)
|
||||
}
|
||||
rpcApi?.let { api ->
|
||||
val upstream = BitcoinUpstream(config.id
|
||||
?: "bitcoin-${seq.getAndIncrement()}", chain, api,
|
||||
options, QuorumForLabels.QuorumItem(1, config.labels),
|
||||
objectMapper, methods)
|
||||
|
||||
upstream.start()
|
||||
currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED))
|
||||
}
|
||||
val upstream = BitcoinUpstream(config.id
|
||||
?: "bitcoin-${seq.getAndIncrement()}", chain, directApi,
|
||||
options, QuorumForLabels.QuorumItem(1, config.labels),
|
||||
objectMapper, methods)
|
||||
|
||||
upstream.start()
|
||||
currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED))
|
||||
}
|
||||
|
||||
private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>,
|
||||
chain: Chain,
|
||||
options: UpstreamsConfig.Options) {
|
||||
val conn = config.connection!!
|
||||
var rpcApi: DirectEthereumApi? = null
|
||||
val directApi: Reader<JsonRpcRequest, JsonRpcResponse>? = buildHttpClient(config)
|
||||
if (directApi == null) {
|
||||
log.warn("Upstream doesn't have API configuration")
|
||||
return
|
||||
}
|
||||
|
||||
val urls = ArrayList<URI>()
|
||||
val methods = buildMethods(config, chain)
|
||||
conn.rpc?.let { endpoint ->
|
||||
val rpcClient = ReactorHttpRpcClient.newBuilder()
|
||||
.connectTo(endpoint.url)
|
||||
.alwaysSeparate()
|
||||
conn.rpc?.basicAuth?.let { auth ->
|
||||
rpcClient.basicAuth(auth.username, auth.password)
|
||||
}
|
||||
conn.rpc?.tls?.let { tls ->
|
||||
tls.ca?.let { ca ->
|
||||
fileResolver.resolve(ca).inputStream().use { cert -> rpcClient.trustedCertificate(cert) }
|
||||
}
|
||||
}
|
||||
rpcApi = DirectEthereumApi(
|
||||
rpcClient.build(),
|
||||
null,
|
||||
objectMapper,
|
||||
methods
|
||||
).apply {
|
||||
timeout = options.timeout
|
||||
}
|
||||
|
||||
urls.add(endpoint.url)
|
||||
}
|
||||
if (rpcApi != null) {
|
||||
val wsApi: EthereumWs? = conn.ws?.let { endpoint ->
|
||||
val wsApi = EthereumWs(
|
||||
endpoint.url,
|
||||
endpoint.origin ?: URI("http://localhost"),
|
||||
rpcApi!!,
|
||||
objectMapper
|
||||
)
|
||||
endpoint.basicAuth?.let { auth ->
|
||||
wsApi.basicAuth = auth
|
||||
}
|
||||
wsApi.connect()
|
||||
urls.add(endpoint.url)
|
||||
wsApi
|
||||
}
|
||||
|
||||
log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}")
|
||||
val ethereumUpstream = EthereumUpstream(
|
||||
config.id!!,
|
||||
chain, rpcApi!!, wsApi, options,
|
||||
QuorumForLabels.QuorumItem(1, config.labels),
|
||||
methods,
|
||||
objectMapper)
|
||||
ethereumUpstream.start()
|
||||
currentUpstreams.update(UpstreamChange(chain, ethereumUpstream, UpstreamChange.ChangeType.ADDED))
|
||||
val wsFactoryApi: EthereumWsFactory? = conn.ws?.let { endpoint ->
|
||||
val wsApi = EthereumWsFactory(
|
||||
endpoint.url,
|
||||
endpoint.origin ?: URI("http://localhost"),
|
||||
objectMapper
|
||||
)
|
||||
endpoint.basicAuth?.let { auth ->
|
||||
wsApi.basicAuth = auth
|
||||
}
|
||||
urls.add(endpoint.url)
|
||||
wsApi
|
||||
}
|
||||
|
||||
log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}")
|
||||
val ethereumUpstream = EthereumUpstream(
|
||||
config.id!!,
|
||||
chain, directApi, wsFactoryApi, options,
|
||||
QuorumForLabels.QuorumItem(1, config.labels),
|
||||
methods,
|
||||
objectMapper
|
||||
)
|
||||
ethereumUpstream.start()
|
||||
currentUpstreams.update(UpstreamChange(chain, ethereumUpstream, UpstreamChange.ChangeType.ADDED))
|
||||
}
|
||||
|
||||
private fun buildGrpcUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options: UpstreamsConfig.Options) {
|
||||
@@ -230,5 +213,22 @@ open class ConfiguredUpstreams(
|
||||
.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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class UpstreamChange(
|
||||
/**
|
||||
* Corresponding upstream
|
||||
*/
|
||||
val upstream: Upstream<*>,
|
||||
val upstream: Upstream,
|
||||
/**
|
||||
* Type of the change
|
||||
*/
|
||||
|
||||
@@ -31,15 +31,24 @@ abstract class AbstractHead : Head {
|
||||
|
||||
private val head = AtomicReference<BlockContainer>(null)
|
||||
private val stream: TopicProcessor<BlockContainer> = TopicProcessor.create()
|
||||
private val beforeBlockHandlers = ArrayList<Runnable>()
|
||||
|
||||
fun follow(source: Flux<BlockContainer>): Disposable {
|
||||
return source.distinctUntilChanged {
|
||||
it.hash
|
||||
}.filter { block ->
|
||||
val curr = head.get()
|
||||
curr == null || curr.difficulty < block.difficulty
|
||||
}
|
||||
return source
|
||||
.distinctUntilChanged {
|
||||
it.hash
|
||||
}.filter { block ->
|
||||
val curr = head.get()
|
||||
curr == null || curr.difficulty < block.difficulty
|
||||
}
|
||||
.doFinally {
|
||||
// close internal stream if upstream is finished, otherwise it gets stuck
|
||||
// but technically is should never happen during normal work, only when the Head
|
||||
// is stopping
|
||||
stream.onComplete()
|
||||
}
|
||||
.subscribe { block ->
|
||||
notifyBeforeBlock()
|
||||
val prev = head.getAndUpdate { curr ->
|
||||
if (curr == null || curr.difficulty < block.difficulty) {
|
||||
block
|
||||
@@ -54,6 +63,20 @@ abstract class AbstractHead : Head {
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyBeforeBlock() {
|
||||
beforeBlockHandlers.forEach { handler ->
|
||||
try {
|
||||
handler.run()
|
||||
} catch (t: Throwable) {
|
||||
log.warn("Before Block handler error", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBeforeBlock(handler: Runnable) {
|
||||
beforeBlockHandlers.add(handler)
|
||||
}
|
||||
|
||||
override fun getFlux(): Flux<BlockContainer> {
|
||||
return Flux.merge(
|
||||
Mono.justOrEmpty(head.get()),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,10 +16,12 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import org.reactivestreams.Publisher
|
||||
|
||||
interface ApiSource<U : UpstreamApi> : Publisher<U> {
|
||||
interface ApiSource : Publisher<Upstream> {
|
||||
|
||||
fun resolve()
|
||||
fun request(tries: Int)
|
||||
|
||||
@@ -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!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]")
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,12 @@ import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -44,14 +42,14 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
@Repository
|
||||
class CurrentUpstreams(
|
||||
class CurrentMultistreamHolder(
|
||||
@Autowired private val objectMapper: ObjectMapper,
|
||||
@Autowired private val cachesFactory: CachesFactory
|
||||
): Upstreams {
|
||||
) : MultistreamHolder {
|
||||
|
||||
private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java)
|
||||
private val log = LoggerFactory.getLogger(CurrentMultistreamHolder::class.java)
|
||||
|
||||
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams<*>>()
|
||||
private val chainMapping = ConcurrentHashMap<Chain, Multistream>()
|
||||
private val chainsBus = TopicProcessor.create<Chain>()
|
||||
private val callTargets = HashMap<Chain, CallMethods>()
|
||||
private val updateLock = ReentrantLock()
|
||||
@@ -61,20 +59,18 @@ class CurrentUpstreams(
|
||||
val chain = change.chain
|
||||
when (BlockchainType.fromBlockchain(chain)) {
|
||||
BlockchainType.ETHEREUM -> {
|
||||
val up = change.upstream
|
||||
.cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream<EthereumApi>
|
||||
val current = chainMapping[chain] as ChainUpstreams<EthereumApi>?
|
||||
val up = change.upstream.cast(EthereumUpstream::class.java)
|
||||
val current = chainMapping[chain] as Multistream?
|
||||
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)
|
||||
}
|
||||
BlockchainType.BITCOIN -> {
|
||||
val up = change.upstream
|
||||
.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
|
||||
val current = chainMapping[chain] as ChainUpstreams<DirectBitcoinApi>?
|
||||
val up = change.upstream.cast(BitcoinUpstream::class.java)
|
||||
val current = chainMapping[chain] as Multistream?
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
if (change.type == UpstreamChange.ChangeType.REMOVED) {
|
||||
current?.removeUpstream(up.getId())
|
||||
@@ -113,7 +109,7 @@ class CurrentUpstreams(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUpstream(chain: Chain): AggregatedUpstream<*>? {
|
||||
override fun getUpstream(chain: Chain): Multistream? {
|
||||
return chainMapping[chain]
|
||||
}
|
||||
|
||||
@@ -22,13 +22,13 @@ import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.TopicProcessor
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
abstract class DefaultUpstream<U : UpstreamApi>(
|
||||
abstract class DefaultUpstream(
|
||||
private val id: String,
|
||||
defaultLag: Long,
|
||||
defaultAvail: UpstreamAvailability,
|
||||
private val options: UpstreamsConfig.Options,
|
||||
private val targets: CallMethods?
|
||||
) : Upstream<U> {
|
||||
) : Upstream {
|
||||
|
||||
constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets)
|
||||
|
||||
|
||||
@@ -25,4 +25,7 @@ class EmptyHead : Head {
|
||||
override fun getFlux(): Flux<BlockContainer> {
|
||||
return Flux.empty()
|
||||
}
|
||||
|
||||
override fun onBeforeBlock(handler: Runnable) {
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import org.reactivestreams.Subscriber
|
||||
import reactor.core.publisher.EmitterProcessor
|
||||
import reactor.core.publisher.Flux
|
||||
@@ -26,28 +29,28 @@ import kotlin.math.pow
|
||||
import kotlin.math.roundToLong
|
||||
import kotlin.random.Random
|
||||
|
||||
class FilteredApis<U : UpstreamApi>(
|
||||
allUpstreams: List<Upstream<U>>,
|
||||
class FilteredApis(
|
||||
allUpstreams: List<Upstream>,
|
||||
private val matcher: Selector.Matcher,
|
||||
pos: Int,
|
||||
private val repeatLimit: Long,
|
||||
jitter: Int
|
||||
) : ApiSource<U> {
|
||||
) : ApiSource {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_DELAY_STEP = 100
|
||||
private const val MAX_WAIT_MILLIS = 5000L
|
||||
}
|
||||
|
||||
constructor(allUpstreams: List<Upstream<U>>,
|
||||
constructor(allUpstreams: List<Upstream>,
|
||||
matcher: Selector.Matcher,
|
||||
pos: Int) : this(allUpstreams, matcher, pos, 10, 7)
|
||||
|
||||
constructor(allUpstreams: List<Upstream<U>>,
|
||||
constructor(allUpstreams: List<Upstream>,
|
||||
matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
|
||||
|
||||
private val delay: Int
|
||||
private val upstreams: List<Upstream<UpstreamApi>>
|
||||
private val upstreams: List<Upstream>
|
||||
|
||||
private val control = EmitterProcessor.create<Boolean>(32, false)
|
||||
|
||||
@@ -75,18 +78,18 @@ class FilteredApis<U : UpstreamApi>(
|
||||
return Duration.ofMillis(time)
|
||||
}
|
||||
|
||||
override fun subscribe(subscriber: Subscriber<in U>) {
|
||||
override fun subscribe(subscriber: Subscriber<in Upstream>) {
|
||||
val first = Flux.fromIterable(upstreams)
|
||||
val retries = (1 until repeatLimit).map { r ->
|
||||
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
|
||||
}.let { Flux.concat(it) }
|
||||
|
||||
Flux.concat(first, retries)
|
||||
.filter(Upstream<UpstreamApi>::isAvailable)
|
||||
.filter(Upstream::isAvailable)
|
||||
.filter(matcher::matches)
|
||||
.flatMap { it.getApi(matcher) }
|
||||
.zipWith(control).map { it.t1 }
|
||||
.subscribe(subscriber as Subscriber<in UpstreamApi>)
|
||||
.zipWith(control)
|
||||
.map { it.t1 }
|
||||
.subscribe(subscriber)
|
||||
}
|
||||
|
||||
override fun resolve() {
|
||||
|
||||
@@ -20,6 +20,20 @@ import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
/**
|
||||
* Subscription to listen to updates to the head of a blockchain.
|
||||
*/
|
||||
interface Head {
|
||||
|
||||
/**
|
||||
* @return stream of all new blocks, starts from the current block (i.e., first item should be available immediately).
|
||||
*/
|
||||
fun getFlux(): Flux<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)
|
||||
}
|
||||
@@ -28,9 +28,9 @@ import reactor.util.function.Tuples
|
||||
* Observer group of upstreams and defined a distance in blocks (lag) between a leader (best height/difficulty) and
|
||||
* other upstreams.
|
||||
*/
|
||||
abstract class HeadLagObserver<A : UpstreamApi>(
|
||||
abstract class HeadLagObserver(
|
||||
private val master: Head,
|
||||
private val followers: Collection<Upstream<A>>
|
||||
private val followers: Collection<Upstream>
|
||||
) : Lifecycle {
|
||||
|
||||
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
|
||||
@@ -58,7 +58,7 @@ abstract class HeadLagObserver<A : UpstreamApi>(
|
||||
}
|
||||
}
|
||||
|
||||
fun probeFollowers(top: BlockContainer): Flux<Tuple2<Long, Upstream<A>>> {
|
||||
fun probeFollowers(top: BlockContainer): Flux<Tuple2<Long, Upstream>> {
|
||||
return Flux.fromIterable(followers)
|
||||
.parallel(followers.size)
|
||||
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
|
||||
@@ -66,9 +66,9 @@ abstract class HeadLagObserver<A : UpstreamApi>(
|
||||
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
|
||||
}
|
||||
|
||||
abstract fun getCurrentBlocks(up: Upstream<A>): Flux<BlockContainer>
|
||||
abstract fun getCurrentBlocks(up: Upstream): Flux<BlockContainer>
|
||||
|
||||
fun mapLagging(top: BlockContainer, up: Upstream<A>, blocks: Flux<BlockContainer>): Flux<Tuple2<Long, Upstream<A>>> {
|
||||
fun mapLagging(top: BlockContainer, up: Upstream, blocks: Flux<BlockContainer>): Flux<Tuple2<Long, Upstream>> {
|
||||
return blocks
|
||||
.map { extractDistance(top, it) }
|
||||
.takeUntil { lag -> lag <= 0L }
|
||||
|
||||
235
src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt
Normal file
235
src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,8 +20,11 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
interface Upstreams {
|
||||
fun getUpstream(chain: Chain): AggregatedUpstream<*>?
|
||||
/**
|
||||
* Holds Multistreams configured for a chain.
|
||||
*/
|
||||
interface MultistreamHolder {
|
||||
fun getUpstream(chain: Chain): Multistream?
|
||||
fun getAvailable(): List<Chain>
|
||||
fun observeChains(): Flux<Chain>
|
||||
fun getDefaultMethods(chain: Chain): CallMethods
|
||||
@@ -96,13 +96,13 @@ class Selector {
|
||||
}
|
||||
|
||||
interface Matcher {
|
||||
fun matches(up: Upstream<UpstreamApi>): Boolean
|
||||
fun matches(up: Upstream): Boolean
|
||||
}
|
||||
|
||||
class MultiMatcher(
|
||||
private val matchers: Collection<Matcher>
|
||||
): Matcher {
|
||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
||||
override fun matches(up: Upstream): Boolean {
|
||||
return matchers.all { it.matches(up) }
|
||||
}
|
||||
|
||||
@@ -114,13 +114,13 @@ class Selector {
|
||||
class MethodMatcher(
|
||||
val method: String
|
||||
): Matcher {
|
||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
||||
override fun matches(up: Upstream): Boolean {
|
||||
return up.getMethods().isAllowed(method)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class LabelSelectorMatcher: Matcher {
|
||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
||||
override fun matches(up: Upstream): Boolean {
|
||||
return up.getLabels().any(this::matches)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class Selector {
|
||||
}
|
||||
|
||||
class EmptyMatcher: Matcher {
|
||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
||||
override fun matches(up: Upstream): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ class Selector {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
||||
override fun matches(up: Upstream): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,19 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
interface Upstream<out A : UpstreamApi> {
|
||||
interface Upstream {
|
||||
fun isAvailable(): Boolean
|
||||
fun getStatus(): UpstreamAvailability
|
||||
fun observeStatus(): Flux<UpstreamAvailability>
|
||||
fun getHead(): Head
|
||||
fun getApi(matcher: Selector.Matcher): Mono<out A>
|
||||
fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse>
|
||||
fun getOptions(): UpstreamsConfig.Options
|
||||
fun setLag(lag: Long)
|
||||
fun getLag(): Long
|
||||
@@ -34,6 +37,5 @@ interface Upstream<out A : UpstreamApi> {
|
||||
fun getMethods(): CallMethods
|
||||
fun getId(): String
|
||||
|
||||
fun <TA : UpstreamApi> castApi(apiType: Class<TA>): Upstream<TA>
|
||||
fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T
|
||||
fun <T : Upstream> cast(selfType: Class<T>): T
|
||||
}
|
||||
@@ -18,24 +18,29 @@ package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.EmptyReader
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
class BitcoinChainUpstreams(
|
||||
open class BitcoinMultistream(
|
||||
chain: Chain,
|
||||
val upstreams: MutableList<BitcoinUpstream>,
|
||||
caches: Caches,
|
||||
objectMapper: ObjectMapper
|
||||
) : ChainUpstreams<DirectBitcoinApi>(chain, upstreams as MutableList<Upstream<DirectBitcoinApi>>, caches, objectMapper) {
|
||||
private val objectMapper: ObjectMapper
|
||||
) : Multistream(chain, upstreams as MutableList<Upstream>, caches), Lifecycle {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java)
|
||||
private val log = LoggerFactory.getLogger(BitcoinMultistream::class.java)
|
||||
}
|
||||
|
||||
private var head: Head? = null
|
||||
private var reader = BitcoinReader(this, EmptyHead(), objectMapper)
|
||||
|
||||
override fun init() {
|
||||
if (upstreams.size > 0) {
|
||||
@@ -68,8 +73,18 @@ class BitcoinChainUpstreams(
|
||||
return head
|
||||
}
|
||||
|
||||
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
|
||||
//TODO
|
||||
return Mono.just(EmptyReader())
|
||||
}
|
||||
|
||||
open fun getReader(): BitcoinReader {
|
||||
return reader
|
||||
}
|
||||
|
||||
override fun setHead(head: Head) {
|
||||
this.head = head
|
||||
reader = BitcoinReader(this, head, objectMapper)
|
||||
}
|
||||
|
||||
override fun getHead(): Head {
|
||||
@@ -80,18 +95,24 @@ class BitcoinChainUpstreams(
|
||||
return upstreams.flatMap { it.getLabels() }
|
||||
}
|
||||
|
||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
||||
if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
|
||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
||||
}
|
||||
return this as Upstream<A>
|
||||
}
|
||||
|
||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
||||
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||
}
|
||||
return castApi(apiType) as T
|
||||
return this as T
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean {
|
||||
return super.isRunning() || reader.isRunning
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
reader.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
reader.stop()
|
||||
}
|
||||
}
|
||||
@@ -15,25 +15,44 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.kotlin.core.publisher.cast
|
||||
|
||||
open class BitcoinReader(
|
||||
api: DirectBitcoinApi,
|
||||
head: Head
|
||||
private val upstreams: BitcoinMultistream,
|
||||
head: Head,
|
||||
private val objectMapper: ObjectMapper
|
||||
) : Lifecycle {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BitcoinReader::class.java)
|
||||
}
|
||||
|
||||
private val mempool = CachingMempoolData(api, head)
|
||||
private val mempool = CachingMempoolData(upstreams, head, objectMapper)
|
||||
|
||||
open fun getMempool(): CachingMempoolData {
|
||||
return mempool
|
||||
}
|
||||
|
||||
open fun getBlock(hash: String): Mono<Map<String, Any>> {
|
||||
return castedRead(JsonRpcRequest("getblock", listOf(hash)), Map::class.java).cast()
|
||||
}
|
||||
|
||||
open fun getTx(txid: String): Mono<Map<String, Any>> {
|
||||
return castedRead(JsonRpcRequest("getrawtransaction", listOf(txid, true)), Map::class.java).cast()
|
||||
}
|
||||
|
||||
open fun listUnspent(): Mono<List<String>> {
|
||||
return castedRead(JsonRpcRequest("listunspent", emptyList()), List::class.java).cast()
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean {
|
||||
return mempool.isRunning
|
||||
}
|
||||
@@ -45,4 +64,14 @@ open class BitcoinReader(
|
||||
override fun stop() {
|
||||
mempool.stop()
|
||||
}
|
||||
|
||||
fun <T> castedRead(req: JsonRpcRequest, clazz: Class<T>): Mono<T> {
|
||||
return upstreams.getDirectApi(Selector.empty).flatMap { api ->
|
||||
api.read(req)
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map {
|
||||
objectMapper.readValue(it, clazz) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,11 @@
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.AbstractHead
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||
@@ -29,7 +32,7 @@ import java.time.Duration
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class BitcoinRpcHead(
|
||||
private val api: DirectBitcoinApi,
|
||||
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||
private val extractBlock: ExtractBlock,
|
||||
private val interval: Duration = Duration.ofSeconds(15)
|
||||
) : Head, AbstractHead(), Lifecycle {
|
||||
@@ -53,12 +56,14 @@ class BitcoinRpcHead(
|
||||
val base = Flux.interval(interval)
|
||||
.publishOn(scheduler)
|
||||
.flatMap {
|
||||
api.executeAndResult(0, "getbestblockhash", emptyList(), String::class.java)
|
||||
api.read(JsonRpcRequest("getbestblockhash", emptyList()))
|
||||
.flatMap(JsonRpcResponse::requireStringResult)
|
||||
.timeout(Defaults.timeout, Mono.error(Exception("Best block hash is not received")))
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flatMap { hash ->
|
||||
api.execute(0, "getblock", listOf(hash))
|
||||
api.read(JsonRpcRequest("getblock", listOf(hash)))
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map(extractBlock::extract)
|
||||
.timeout(Defaults.timeout, Mono.error(Exception("Block data is not received")))
|
||||
}
|
||||
|
||||
@@ -17,9 +17,12 @@ package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
@@ -29,12 +32,12 @@ import reactor.core.publisher.Mono
|
||||
open class BitcoinUpstream(
|
||||
id: String,
|
||||
val chain: Chain,
|
||||
private val api: DirectBitcoinApi,
|
||||
private val directApi: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||
options: UpstreamsConfig.Options,
|
||||
val node: QuorumForLabels.QuorumItem,
|
||||
private val objectMapper: ObjectMapper,
|
||||
callMethods: CallMethods
|
||||
) : DefaultUpstream<DirectBitcoinApi>(id, options, callMethods), Lifecycle {
|
||||
) : DefaultUpstream(id, options, callMethods), Lifecycle {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
|
||||
@@ -42,43 +45,31 @@ open class BitcoinUpstream(
|
||||
|
||||
private val head: Head = createHead()
|
||||
private var validatorSubscription: Disposable? = null
|
||||
private val data = BitcoinReader(api, head)
|
||||
|
||||
private fun createHead(): Head {
|
||||
return BitcoinRpcHead(
|
||||
api,
|
||||
directApi,
|
||||
ExtractBlock(objectMapper)
|
||||
)
|
||||
}
|
||||
|
||||
open fun getData(): BitcoinReader {
|
||||
return data
|
||||
}
|
||||
|
||||
override fun getHead(): Head {
|
||||
return head
|
||||
}
|
||||
|
||||
override fun getApi(matcher: Selector.Matcher): Mono<out DirectBitcoinApi> {
|
||||
return Mono.just(api)
|
||||
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
return directApi
|
||||
}
|
||||
|
||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||
return listOf(UpstreamsConfig.Labels())
|
||||
}
|
||||
|
||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
||||
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||
}
|
||||
return castApi(apiType) as T
|
||||
}
|
||||
|
||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
||||
if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
|
||||
throw ClassCastException("Cannot cast ${DirectBitcoinApi::class.java} to $apiType")
|
||||
}
|
||||
return this as Upstream<A>
|
||||
return this as T
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean {
|
||||
@@ -86,7 +77,7 @@ open class BitcoinUpstream(
|
||||
if (head is Lifecycle) {
|
||||
runningAny = runningAny || head.isRunning
|
||||
}
|
||||
runningAny = runningAny || data.isRunning
|
||||
runningAny = runningAny
|
||||
return runningAny
|
||||
}
|
||||
|
||||
@@ -97,7 +88,6 @@ open class BitcoinUpstream(
|
||||
head.start()
|
||||
}
|
||||
}
|
||||
data.start()
|
||||
|
||||
validatorSubscription?.dispose()
|
||||
|
||||
@@ -105,7 +95,7 @@ open class BitcoinUpstream(
|
||||
this.setLag(0)
|
||||
this.setStatus(UpstreamAvailability.OK)
|
||||
} else {
|
||||
val validator = BitcoinUpstreamValidator(api, getOptions())
|
||||
val validator = BitcoinUpstreamValidator(directApi, getOptions())
|
||||
validatorSubscription = validator.start()
|
||||
.subscribe(this::setStatus)
|
||||
}
|
||||
@@ -115,9 +105,7 @@ open class BitcoinUpstream(
|
||||
if (head is Lifecycle) {
|
||||
head.stop()
|
||||
}
|
||||
data.stop()
|
||||
validatorSubscription?.dispose()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -16,7 +16,10 @@
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||
import reactor.core.publisher.Flux
|
||||
@@ -26,7 +29,7 @@ import java.time.Duration
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class BitcoinUpstreamValidator(
|
||||
private val api: DirectBitcoinApi,
|
||||
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||
private val options: UpstreamsConfig.Options
|
||||
) {
|
||||
|
||||
@@ -36,7 +39,9 @@ class BitcoinUpstreamValidator(
|
||||
}
|
||||
|
||||
fun validate(): Mono<UpstreamAvailability> {
|
||||
return api.executeAndResult(0, "getconnectioncount", emptyList(), Int::class.java)
|
||||
return api.read(JsonRpcRequest("getconnectioncount", emptyList()))
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map { Integer.parseInt(String(it)) }
|
||||
.map { count ->
|
||||
val minPeers = options.minPeers ?: 1
|
||||
if (count < minPeers) {
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.Disposable
|
||||
@@ -26,8 +30,9 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
open class CachingMempoolData(
|
||||
private val api: DirectBitcoinApi,
|
||||
private val head: Head
|
||||
private val upstreams: BitcoinMultistream,
|
||||
private val head: Head,
|
||||
private val objectMapper: ObjectMapper
|
||||
) : Lifecycle {
|
||||
|
||||
companion object {
|
||||
@@ -56,7 +61,11 @@ open class CachingMempoolData(
|
||||
}
|
||||
|
||||
fun fetchFromUpstream(): Mono<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>) {
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,6 +63,7 @@ class ExtractBlock(
|
||||
getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"),
|
||||
false,
|
||||
json,
|
||||
data,
|
||||
transactions
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class AggregatedCallMethods(
|
||||
*/
|
||||
override fun getQuorumFor(method: String): CallQuorum {
|
||||
return delegates.find {
|
||||
it.isAllowed(method)
|
||||
it.isAllowed(method) || it.isHardcoded(method)
|
||||
}?.getQuorumFor(method) ?: throw IllegalStateException("No quorum for $method")
|
||||
}
|
||||
|
||||
@@ -62,15 +62,15 @@ class AggregatedCallMethods(
|
||||
* @return true if there is at least one delegate that allows the method and it's hardcoded on that delegate
|
||||
*/
|
||||
override fun isHardcoded(method: String): Boolean {
|
||||
return delegates.any { it.isAllowed(method) && it.isHardcoded(method) }
|
||||
return delegates.any { it.isHardcoded(method) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed the method on the first delegate that supports it as a hardcoded method
|
||||
*/
|
||||
override fun executeHardcoded(method: String): Any {
|
||||
override fun executeHardcoded(method: String): ByteArray {
|
||||
return delegates.find {
|
||||
it.isAllowed(method) && it.isHardcoded(method)
|
||||
it.isHardcoded(method)
|
||||
}?.executeHardcoded(method) ?: throw IllegalStateException("No hardcoded for $method")
|
||||
}
|
||||
}
|
||||
@@ -46,5 +46,5 @@ interface CallMethods {
|
||||
/**
|
||||
* Read [supposed to be predefined] method from this config
|
||||
*/
|
||||
fun executeHardcoded(method: String): Any
|
||||
fun executeHardcoded(method: String): ByteArray
|
||||
}
|
||||
@@ -13,12 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
package io.emeraldpay.dshackle.upstream.calls
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.quorum.*
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import java.util.*
|
||||
|
||||
@@ -26,9 +24,6 @@ class DefaultBitcoinMethods(
|
||||
private val objectMapper: ObjectMapper
|
||||
) : CallMethods {
|
||||
|
||||
//TODO maybe Ethereum RPC parser should not be really used for Bitcoin
|
||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
||||
|
||||
private val anyResponseMethods = listOf(
|
||||
"getblock",
|
||||
"gettransaction", "getrawtransaction", "gettxout",
|
||||
@@ -55,7 +50,7 @@ class DefaultBitcoinMethods(
|
||||
Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum()
|
||||
Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2)
|
||||
Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0)
|
||||
Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(jacksonRpcConverter)
|
||||
Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(objectMapper)
|
||||
else -> AlwaysQuorum()
|
||||
}
|
||||
}
|
||||
@@ -72,13 +67,10 @@ class DefaultBitcoinMethods(
|
||||
return Collections.binarySearch(hardcodedMethods, method) >= 0;
|
||||
}
|
||||
|
||||
override fun executeHardcoded(method: String): Any {
|
||||
override fun executeHardcoded(method: String): ByteArray {
|
||||
return when (method) {
|
||||
"getconnectioncount" -> 42
|
||||
"getnetworkinfo" -> mapOf(
|
||||
"version" to 700000,
|
||||
"subversion" to "/EmeraldDshackle:v0.7/"
|
||||
)
|
||||
"getconnectioncount" -> "42".toByteArray()
|
||||
"getnetworkinfo" -> "{\"version\": 700000, \"subversion\": \"/EmeraldDshackle:v0.7/\"}".toByteArray()
|
||||
else -> throw RpcException(-32601, "Method not found")
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,6 @@ class DefaultEthereumMethods(
|
||||
private val chain: Chain
|
||||
) : CallMethods {
|
||||
|
||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
||||
|
||||
private val anyResponseMethods = listOf(
|
||||
"eth_gasPrice",
|
||||
"eth_call",
|
||||
@@ -90,9 +88,9 @@ class DefaultEthereumMethods(
|
||||
headVerifiedMethods.contains(method) -> NotLaggingQuorum(1)
|
||||
specialMethods.contains(method) -> {
|
||||
when (method) {
|
||||
"eth_getTransactionCount" -> NonceQuorum(jacksonRpcConverter)
|
||||
"eth_getTransactionCount" -> NonceQuorum(objectMapper)
|
||||
"eth_getBalance" -> NotLaggingQuorum(1)
|
||||
"eth_sendRawTransaction" -> BroadcastQuorum(jacksonRpcConverter)
|
||||
"eth_sendRawTransaction" -> BroadcastQuorum(objectMapper)
|
||||
else -> AlwaysQuorum()
|
||||
}
|
||||
}
|
||||
@@ -108,50 +106,55 @@ class DefaultEthereumMethods(
|
||||
return hardcodedMethods.contains(method)
|
||||
}
|
||||
|
||||
override fun executeHardcoded(method: String): Any {
|
||||
if ("net_version" == method) {
|
||||
if (Chain.ETHEREUM == chain) {
|
||||
return "1"
|
||||
override fun executeHardcoded(method: String): ByteArray {
|
||||
val json = when (method) {
|
||||
"net_version" -> {
|
||||
when {
|
||||
Chain.ETHEREUM == chain -> {
|
||||
"1"
|
||||
}
|
||||
Chain.ETHEREUM_CLASSIC == chain -> {
|
||||
"1"
|
||||
}
|
||||
Chain.TESTNET_MORDEN == chain -> {
|
||||
"2"
|
||||
}
|
||||
Chain.TESTNET_KOVAN == chain -> {
|
||||
"42"
|
||||
}
|
||||
else -> throw RpcException(-32602, "Invalid chain")
|
||||
}
|
||||
}
|
||||
if (Chain.ETHEREUM_CLASSIC == chain) {
|
||||
return "1"
|
||||
"net_peerCount" -> {
|
||||
"\"0x2a\""
|
||||
}
|
||||
if (Chain.TESTNET_MORDEN == chain) {
|
||||
return "2"
|
||||
"net_listening" -> {
|
||||
"true"
|
||||
}
|
||||
if (Chain.TESTNET_KOVAN == chain) {
|
||||
return "42"
|
||||
"web3_clientVersion" -> {
|
||||
"\"EmeraldDshackle/v0.2\""
|
||||
}
|
||||
throw RpcException(-32602, "Invalid chain")
|
||||
"eth_protocolVersion" -> {
|
||||
"\"0x3f\""
|
||||
}
|
||||
"eth_syncing" -> {
|
||||
"false"
|
||||
}
|
||||
"eth_coinbase" -> {
|
||||
"\"0x0000000000000000000000000000000000000000\""
|
||||
}
|
||||
"eth_mining" -> {
|
||||
"false"
|
||||
}
|
||||
"eth_hashrate" -> {
|
||||
"\"0x0\""
|
||||
}
|
||||
"eth_accounts" -> {
|
||||
"[]"
|
||||
}
|
||||
else -> throw RpcException(-32601, "Method not found")
|
||||
}
|
||||
if ("net_peerCount" == method) {
|
||||
return "0x2a"
|
||||
}
|
||||
if ("net_listening" == method) {
|
||||
return true
|
||||
}
|
||||
if ("web3_clientVersion" == method) {
|
||||
return "EmeraldDshackle/v0.2"
|
||||
}
|
||||
if ("eth_protocolVersion" == method) {
|
||||
return "0x3f"
|
||||
}
|
||||
if ("eth_syncing" == method) {
|
||||
return false
|
||||
}
|
||||
if ("eth_coinbase" == method) {
|
||||
return "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
if ("eth_mining" == method) {
|
||||
return "false"
|
||||
}
|
||||
if ("eth_hashrate" == method) {
|
||||
return "0x0"
|
||||
}
|
||||
if ("eth_accounts" == method) {
|
||||
return Collections.emptyList<String>()
|
||||
}
|
||||
throw RpcException(-32601, "Method not found")
|
||||
return json.toByteArray()
|
||||
}
|
||||
|
||||
override fun getSupportedMethods(): Set<String> {
|
||||
|
||||
@@ -44,7 +44,7 @@ open class DirectCallMethods(private val methods: Set<String>) : CallMethods {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun executeHardcoded(method: String): Any {
|
||||
return "unsupported"
|
||||
override fun executeHardcoded(method: String): ByteArray {
|
||||
return "unsupported".toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class ManagedCallMethods(
|
||||
return delegate.isHardcoded(method)
|
||||
}
|
||||
|
||||
override fun executeHardcoded(method: String): Any {
|
||||
override fun executeHardcoded(method: String): ByteArray {
|
||||
return delegate.executeHardcoded(method)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.emeraldpay.dshackle.cache
|
||||
package io.emeraldpay.dshackle.upstream.ethereum
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
@@ -30,20 +30,20 @@ import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
/**
|
||||
* Reads blocks with full transactions details. Based on data contained in cashes for blocks
|
||||
* and transactions, i.e. two separate caches that must be provided.
|
||||
* Reads blocks with full transactions details. Based on data contained in readers for blocks
|
||||
* and transactions, i.e. two separate readers that must be provided.
|
||||
*
|
||||
* If source block, with just transaction hashes is not available, it returns empty
|
||||
* If any of the expected block transactions is not available it returns empty
|
||||
*/
|
||||
class EthereumBlocksWithTxCache(
|
||||
class EthereumFullBlocksReader(
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val blocks: Reader<BlockId, BlockContainer>,
|
||||
private val txes: Reader<TxId, TxContainer>
|
||||
) : Reader<BlockId, BlockContainer> {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(EthereumBlocksWithTxCache::class.java)
|
||||
private val log = LoggerFactory.getLogger(EthereumFullBlocksReader::class.java)
|
||||
}
|
||||
|
||||
override fun read(key: BlockId): Mono<BlockContainer> {
|
||||
@@ -26,14 +26,14 @@ import java.time.Duration
|
||||
|
||||
class EthereumHeadLagObserver(
|
||||
master: Head,
|
||||
followers: Collection<Upstream<EthereumApi>>
|
||||
) : HeadLagObserver<EthereumApi>(master, followers) {
|
||||
followers: Collection<Upstream>
|
||||
) : HeadLagObserver(master, followers) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(EthereumHeadLagObserver::class.java)
|
||||
}
|
||||
|
||||
override fun getCurrentBlocks(up: Upstream<EthereumApi>): Flux<BlockContainer> {
|
||||
override fun getCurrentBlocks(up: Upstream): Flux<BlockContainer> {
|
||||
val head = up.getHead()
|
||||
return head.getFlux().take(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
@@ -19,20 +19,24 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
open class AggregatedEthereumUpstreams(
|
||||
open class EthereumMultistream(
|
||||
chain: Chain,
|
||||
val upstreams: MutableList<EthereumUpstream>,
|
||||
caches: Caches,
|
||||
objectMapper: ObjectMapper
|
||||
) : ChainUpstreams<EthereumApi>(chain, upstreams as MutableList<Upstream<EthereumApi>>, caches, objectMapper) {
|
||||
private val objectMapper: ObjectMapper
|
||||
) : Multistream(chain, upstreams as MutableList<Upstream>, caches) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AggregatedEthereumUpstreams::class.java)
|
||||
private val log = LoggerFactory.getLogger(EthereumMultistream::class.java)
|
||||
}
|
||||
|
||||
private var head: Head? = null
|
||||
@@ -92,7 +96,7 @@ open class AggregatedEthereumUpstreams(
|
||||
val newHead = MergedHead(upstreams.map { it.getHead() }).apply {
|
||||
this.start()
|
||||
}
|
||||
val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection<Upstream<EthereumApi>>).apply {
|
||||
val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection<Upstream>).apply {
|
||||
this.start()
|
||||
}
|
||||
this.lagObserver = lagObserver
|
||||
@@ -107,18 +111,15 @@ open class AggregatedEthereumUpstreams(
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
||||
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||
}
|
||||
return castApi(apiType) as T
|
||||
return this as T
|
||||
}
|
||||
|
||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
||||
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
|
||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
||||
}
|
||||
return this as Upstream<A>
|
||||
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
|
||||
return Mono.just(NativeCallRouter(objectMapper, reader, getMethods()))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,24 +18,21 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||
import io.emeraldpay.dshackle.cache.CurrentBlockCache
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.data.BlockId
|
||||
import io.emeraldpay.dshackle.data.TxContainer
|
||||
import io.emeraldpay.dshackle.data.TxId
|
||||
import io.emeraldpay.dshackle.data.*
|
||||
import io.emeraldpay.dshackle.reader.*
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.infinitape.etherjar.domain.Address
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.domain.Wei
|
||||
import io.infinitape.etherjar.rpc.Commands
|
||||
import io.infinitape.etherjar.rpc.RpcCall
|
||||
import io.infinitape.etherjar.hex.HexQuantity
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import io.infinitape.etherjar.rpc.json.BlockTag
|
||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -48,7 +45,7 @@ import java.util.concurrent.TimeoutException
|
||||
import java.util.function.Function
|
||||
|
||||
open class EthereumReader(
|
||||
private val up: Upstream<EthereumApi>,
|
||||
private val up: Multistream,
|
||||
private val caches: Caches,
|
||||
private val objectMapper: ObjectMapper
|
||||
) : Lifecycle {
|
||||
@@ -57,51 +54,109 @@ open class EthereumReader(
|
||||
private val log = LoggerFactory.getLogger(EthereumReader::class.java)
|
||||
}
|
||||
|
||||
private var headListener: Disposable? = null
|
||||
private val balanceCache = CurrentBlockCache<Address, Wei>()
|
||||
|
||||
private val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { block ->
|
||||
objectMapper
|
||||
.readValue(block.json, BlockJson::class.java)
|
||||
.withoutTransactionDetails()
|
||||
val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { block ->
|
||||
val existing = block.getParsed(BlockJson::class.java)
|
||||
if (existing != null) {
|
||||
existing.withoutTransactionDetails()
|
||||
} else {
|
||||
objectMapper
|
||||
.readValue(block.json, BlockJson::class.java)
|
||||
.withoutTransactionDetails()
|
||||
}
|
||||
}
|
||||
|
||||
private val extractTx = Function<TxContainer, TransactionJson> { tx ->
|
||||
objectMapper
|
||||
.readValue(tx.json, TransactionJson::class.java)
|
||||
val extractTx = Function<TxContainer, TransactionJson> { tx ->
|
||||
tx.getParsed(TransactionJson::class.java) ?: objectMapper.readValue(tx.json, TransactionJson::class.java)
|
||||
}
|
||||
|
||||
private val blocksDirect: Reader<BlockHash, BlockJson<TransactionRefJson>>
|
||||
private val txDirect: Reader<TransactionId, TransactionJson>
|
||||
val asRaw = Function<SourceContainer, ByteArray> { tx ->
|
||||
tx.json ?: ByteArray(0)
|
||||
}
|
||||
|
||||
val jsonToRaw = Function<Any, ByteArray> { json ->
|
||||
objectMapper.writeValueAsBytes(json)
|
||||
}
|
||||
|
||||
val blockAsContainer = Function<BlockJson<*>, BlockContainer> { block ->
|
||||
BlockContainer.from(block.withoutTransactionDetails(), objectMapper)
|
||||
}
|
||||
val txAsContainer = Function<TransactionJson, TxContainer> { tx ->
|
||||
TxContainer.from(tx, objectMapper)
|
||||
}
|
||||
|
||||
private val blocksDirect: Reader<BlockHash, BlockContainer>
|
||||
private val blocksByHeightDirect: Reader<Long, BlockContainer>
|
||||
private val txDirect: Reader<TransactionId, TxContainer>
|
||||
private val balanceDirect: Reader<Address, Wei>
|
||||
|
||||
private val idToBlockHash = Function<BlockId, BlockHash> { id -> BlockHash.from(id.value) }
|
||||
private val blockHashToId = Function<BlockHash, BlockId> { hash -> BlockId.from(hash) }
|
||||
|
||||
private val txHashToId = Function<TransactionId, TxId> { hash -> TxId.from(hash) }
|
||||
private val idToTxHash = Function<TxId, TransactionId> { id -> TransactionId.from(id.value) }
|
||||
|
||||
private val directResponseBytes = Function<JsonRpcResponse, ByteArray> { resp ->
|
||||
if (resp.error != null) {
|
||||
throw resp.error.asException()
|
||||
} else {
|
||||
resp.getResult()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
blocksDirect = object : Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
||||
override fun read(key: BlockHash): Mono<BlockJson<TransactionRefJson>> {
|
||||
return up.getApi(Selector.empty).flatMap { api ->
|
||||
api.executeAndConvert(Commands.eth().getBlock(key))
|
||||
blocksDirect = object : Reader<BlockHash, BlockContainer> {
|
||||
override fun read(key: BlockHash): Mono<BlockContainer> {
|
||||
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||
val request = JsonRpcRequest("eth_getBlockByHash", listOf(key.toHex(), false))
|
||||
api.read(request)
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key")))
|
||||
.map(directResponseBytes)
|
||||
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||
.map { blockbytes ->
|
||||
val block = objectMapper.readValue(blockbytes, BlockJson::class.java) as BlockJson<TransactionRefJson>
|
||||
BlockContainer.from(block, blockbytes)
|
||||
}
|
||||
.doOnNext { block ->
|
||||
caches.cache(Caches.Tag.REQUESTED, BlockContainer.from(block, objectMapper))
|
||||
caches.cache(Caches.Tag.REQUESTED, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
txDirect = object : Reader<TransactionId, TransactionJson> {
|
||||
override fun read(key: TransactionId): Mono<TransactionJson> {
|
||||
return up.getApi(Selector.empty).flatMap { api ->
|
||||
api.executeAndConvert(Commands.eth().getTransaction(key))
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key")))
|
||||
blocksByHeightDirect = object : Reader<Long, BlockContainer> {
|
||||
override fun read(key: Long): Mono<BlockContainer> {
|
||||
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||
val request = JsonRpcRequest("eth_getBlockByNumber", listOf(HexQuantity.from(key).toHex(), false))
|
||||
api.read(request)
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key")))
|
||||
.map(directResponseBytes)
|
||||
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||
.map { blockbytes ->
|
||||
val block = objectMapper.readValue(blockbytes, BlockJson::class.java) as BlockJson<TransactionRefJson>
|
||||
BlockContainer.from(block, blockbytes)
|
||||
}
|
||||
.doOnNext { block ->
|
||||
caches.cache(Caches.Tag.REQUESTED, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
txDirect = object : Reader<TransactionId, TxContainer> {
|
||||
override fun read(key: TransactionId): Mono<TxContainer> {
|
||||
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||
val request = JsonRpcRequest("eth_getTransactionByHash", listOf(key.toHex()))
|
||||
api.read(request)
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key")))
|
||||
.map(directResponseBytes)
|
||||
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||
.map { txbytes ->
|
||||
val tx = objectMapper.readValue(txbytes, TransactionJson::class.java)
|
||||
TxContainer.from(tx, txbytes)
|
||||
}
|
||||
.doOnNext { tx ->
|
||||
if (tx.blockNumber != null && tx.blockHash != null) {
|
||||
caches.cache(Caches.Tag.REQUESTED, TxContainer.from(tx, objectMapper))
|
||||
if (tx.blockId != null) {
|
||||
caches.cache(Caches.Tag.REQUESTED, tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,9 +164,19 @@ open class EthereumReader(
|
||||
}
|
||||
balanceDirect = object : Reader<Address, Wei> {
|
||||
override fun read(key: Address): Mono<Wei> {
|
||||
return up.getApi(Selector.empty).flatMap { api ->
|
||||
api.executeAndConvert(Commands.eth().getBalance(key, BlockTag.LATEST))
|
||||
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||
val request = JsonRpcRequest("eth_getBalance", listOf(key.toHex(), "latest"))
|
||||
api.read(request)
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Balance not read $key")))
|
||||
.map(directResponseBytes)
|
||||
.map {
|
||||
val str = String(it)
|
||||
if (str.startsWith("\"") && str.endsWith("\"")) {
|
||||
Wei.from(str.substring(1, str.length - 1))
|
||||
} else {
|
||||
throw RpcException(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Not Wei value")
|
||||
}
|
||||
}
|
||||
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||
.doOnNext { value ->
|
||||
balanceCache.put(key, value)
|
||||
@@ -121,30 +186,61 @@ open class EthereumReader(
|
||||
}
|
||||
}
|
||||
|
||||
fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> {
|
||||
return CompoundReader(
|
||||
TransformingReader(caches.getBlocksByHash(), extractBlock),
|
||||
RekeyingReader(idToBlockHash, blocksDirect)
|
||||
fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
||||
return TransformingReader(
|
||||
CompoundReader(
|
||||
RekeyingReader(blockHashToId, caches.getBlocksByHash()),
|
||||
blocksDirect
|
||||
),
|
||||
extractBlock
|
||||
)
|
||||
}
|
||||
|
||||
fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
||||
return CompoundReader(
|
||||
RekeyingReader(
|
||||
blockHashToId,
|
||||
TransformingReader(caches.getBlocksByHash(), extractBlock)
|
||||
fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> {
|
||||
return TransformingReader(
|
||||
CompoundReader(
|
||||
caches.getBlocksByHash(),
|
||||
RekeyingReader(idToBlockHash, blocksDirect)
|
||||
),
|
||||
blocksDirect
|
||||
extractBlock
|
||||
)
|
||||
}
|
||||
|
||||
fun blocksByHashAsCont(): Reader<BlockHash, BlockContainer> {
|
||||
return TransformingReader(
|
||||
blocksByHash(),
|
||||
blockAsContainer
|
||||
)
|
||||
}
|
||||
|
||||
fun blocksByIdAsCont(): Reader<BlockId, BlockContainer> {
|
||||
return TransformingReader(
|
||||
blocksById(),
|
||||
blockAsContainer
|
||||
)
|
||||
}
|
||||
|
||||
fun blocksByHeightAsCont(): Reader<Long, BlockContainer> {
|
||||
return CompoundReader(
|
||||
caches.getBlocksByHeight(),
|
||||
blocksByHeightDirect
|
||||
)
|
||||
}
|
||||
|
||||
fun txByHash(): Reader<TransactionId, TransactionJson> {
|
||||
return CompoundReader(
|
||||
RekeyingReader(
|
||||
txHashToId,
|
||||
TransformingReader(caches.getTxByHash(), extractTx)
|
||||
return TransformingReader(
|
||||
CompoundReader(
|
||||
RekeyingReader(txHashToId, caches.getTxByHash()),
|
||||
txDirect
|
||||
),
|
||||
txDirect
|
||||
extractTx
|
||||
)
|
||||
}
|
||||
|
||||
fun txByHashAsCont(): Reader<TxId, TxContainer> {
|
||||
return CompoundReader(
|
||||
caches.getTxByHash(),
|
||||
RekeyingReader(idToTxHash, txDirect)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,18 +251,17 @@ open class EthereumReader(
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean {
|
||||
return this.headListener != null
|
||||
//TODO should be always running?
|
||||
return up.isRunning
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
this.headListener = up.getHead().getFlux().subscribe {
|
||||
val evictCaches: Runnable = Runnable {
|
||||
balanceCache.evict()
|
||||
}
|
||||
up.getHead().onBeforeBlock(evictCaches)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
val headListener = this.headListener
|
||||
this.headListener = null
|
||||
headListener?.dispose()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.infinitape.etherjar.hex.HexQuantity
|
||||
import io.infinitape.etherjar.rpc.Commands
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
@@ -31,7 +35,7 @@ import java.time.Duration
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class EthereumRpcHead(
|
||||
private val api: DirectEthereumApi,
|
||||
private val api: Reader<in JsonRpcRequest, out JsonRpcResponse>,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val interval: Duration = Duration.ofSeconds(10)
|
||||
): DefaultEthereumHead(), Lifecycle {
|
||||
@@ -48,21 +52,27 @@ class EthereumRpcHead(
|
||||
val base = Flux.interval(interval)
|
||||
.publishOn(scheduler)
|
||||
.flatMap {
|
||||
api.rpcClient
|
||||
.execute(Commands.eth().blockNumber)
|
||||
api.read(JsonRpcRequest("eth_blockNumber", emptyList()))
|
||||
.subscribeOn(scheduler)
|
||||
.timeout(Defaults.timeout, Mono.error(Exception("Block number not received")))
|
||||
.flatMap {
|
||||
if (it.error != null) {
|
||||
Mono.error(it.error.asException())
|
||||
} else {
|
||||
val value = it.getResultAsProcessedString()
|
||||
Mono.just(HexQuantity.from(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
//fetching by Block Height here, critical to use same upstream,
|
||||
//different upstreams may have different blocks on the same height
|
||||
api.rpcClient
|
||||
.execute(Commands.eth().getBlock(it))
|
||||
api.read(JsonRpcRequest("eth_getBlockByNumber", listOf(it.toHex(), false)))
|
||||
.subscribeOn(scheduler)
|
||||
.timeout(Defaults.timeout, Mono.error(Exception("Block data not received")))
|
||||
}
|
||||
.map {
|
||||
BlockContainer.from(it, objectMapper)
|
||||
BlockContainer.from(it.getResult(), objectMapper)
|
||||
}
|
||||
.onErrorContinue { err, _ ->
|
||||
log.debug("RPC error ${err.message}")
|
||||
|
||||
@@ -20,10 +20,13 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
@@ -34,15 +37,15 @@ import java.time.Duration
|
||||
open class EthereumUpstream(
|
||||
id: String,
|
||||
val chain: Chain,
|
||||
private val api: DirectEthereumApi,
|
||||
private val ethereumWs: EthereumWs? = null,
|
||||
private val directReader: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||
private val ethereumWsFactory: EthereumWsFactory? = null,
|
||||
options: UpstreamsConfig.Options,
|
||||
val node: QuorumForLabels.QuorumItem,
|
||||
targets: CallMethods,
|
||||
private val objectMapper: ObjectMapper
|
||||
) : DefaultUpstream<EthereumApi>(id, options, targets), Upstream<EthereumApi>, CachesEnabled, Lifecycle {
|
||||
) : DefaultUpstream(id, options, targets), Upstream, CachesEnabled, Lifecycle {
|
||||
|
||||
constructor(id: String, chain: Chain, api: DirectEthereumApi, objectMapper: ObjectMapper) : this(id, chain, api, null,
|
||||
constructor(id: String, chain: Chain, api: Reader<JsonRpcRequest, JsonRpcResponse>, objectMapper: ObjectMapper) : this(id, chain, api, null,
|
||||
UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
||||
DirectCallMethods(), objectMapper)
|
||||
|
||||
@@ -52,12 +55,7 @@ open class EthereumUpstream(
|
||||
private val head: Head = this.createHead()
|
||||
private var validatorSubscription: Disposable? = null
|
||||
|
||||
init {
|
||||
api.upstream = this
|
||||
}
|
||||
|
||||
override fun setCaches(caches: Caches) {
|
||||
api.caches = caches;
|
||||
if (head is CachesEnabled) {
|
||||
head.setCaches(caches)
|
||||
}
|
||||
@@ -70,7 +68,7 @@ open class EthereumUpstream(
|
||||
this.setLag(0)
|
||||
this.setStatus(UpstreamAvailability.OK)
|
||||
} else {
|
||||
val validator = EthereumUpstreamValidator(this, getOptions())
|
||||
val validator = EthereumUpstreamValidator(this, getOptions(), objectMapper)
|
||||
validatorSubscription = validator.start()
|
||||
.subscribe(this::setStatus)
|
||||
}
|
||||
@@ -89,21 +87,24 @@ open class EthereumUpstream(
|
||||
}
|
||||
|
||||
open fun createHead(): Head {
|
||||
return if (ethereumWs != null) {
|
||||
val ws = EthereumWsHead(ethereumWs).apply {
|
||||
this.start()
|
||||
return if (ethereumWsFactory != null) {
|
||||
val ws = ethereumWsFactory.create(this).apply {
|
||||
connect()
|
||||
}
|
||||
// receive bew blocks through Websockets, but periodically verify with RPC
|
||||
val rpc = EthereumRpcHead(api, objectMapper, Duration.ofSeconds(30)).apply {
|
||||
this.start()
|
||||
val wsHead = EthereumWsHead(ws).apply {
|
||||
start()
|
||||
}
|
||||
MergedHead(listOf(rpc, ws)).apply {
|
||||
this.start()
|
||||
// receive bew blocks through WebSockets, but also periodically verify with RPC in case if WS failed
|
||||
val rpcHead = EthereumRpcHead(getApi(), objectMapper, Duration.ofSeconds(60)).apply {
|
||||
start()
|
||||
}
|
||||
MergedHead(listOf(rpcHead, wsHead)).apply {
|
||||
start()
|
||||
}
|
||||
} else {
|
||||
log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC")
|
||||
EthereumRpcHead(api, objectMapper).apply {
|
||||
this.start()
|
||||
EthereumRpcHead(getApi(), objectMapper).apply {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,8 +113,8 @@ open class EthereumUpstream(
|
||||
return head
|
||||
}
|
||||
|
||||
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> {
|
||||
return Mono.just(api)
|
||||
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
return directReader
|
||||
}
|
||||
|
||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||
@@ -121,18 +122,11 @@ open class EthereumUpstream(
|
||||
}
|
||||
|
||||
@Suppress("unchecked")
|
||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
||||
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||
}
|
||||
return castApi(apiType) as T
|
||||
}
|
||||
|
||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
||||
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
|
||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
||||
}
|
||||
return this as Upstream<A>
|
||||
return this as T
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,11 +16,13 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.ethereum
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
import io.infinitape.etherjar.rpc.*
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.infinitape.etherjar.rpc.json.SyncingJson
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||
import reactor.core.publisher.Flux
|
||||
@@ -30,8 +32,9 @@ import java.time.Duration
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class EthereumUpstreamValidator(
|
||||
private val ethereumUpstream: EthereumUpstream,
|
||||
private val options: UpstreamsConfig.Options
|
||||
private val upstream: EthereumUpstream,
|
||||
private val options: UpstreamsConfig.Options,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java)
|
||||
@@ -39,30 +42,32 @@ class EthereumUpstreamValidator(
|
||||
}
|
||||
|
||||
fun validate(): Mono<UpstreamAvailability> {
|
||||
return ethereumUpstream
|
||||
.getApi(Selector.empty)
|
||||
.flatMapMany { api ->
|
||||
api.rpcClient
|
||||
.execute(Commands.eth().syncing())
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing")))
|
||||
.flatMap { value ->
|
||||
if (value.isSyncing) {
|
||||
Mono.just(UpstreamAvailability.SYNCING)
|
||||
} else {
|
||||
api.rpcClient.execute(Commands.net().peerCount())
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers")))
|
||||
.map { count ->
|
||||
val minPeers = options.minPeers ?: 1
|
||||
if (count < minPeers) {
|
||||
UpstreamAvailability.IMMATURE
|
||||
} else {
|
||||
UpstreamAvailability.OK
|
||||
}
|
||||
}
|
||||
return upstream
|
||||
.getApi()
|
||||
.read(JsonRpcRequest("eth_syncing", listOf()))
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map { objectMapper.readValue(it, SyncingJson::class.java) }
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing")))
|
||||
.flatMap { value ->
|
||||
if (value.isSyncing) {
|
||||
Mono.just(UpstreamAvailability.SYNCING)
|
||||
} else {
|
||||
upstream
|
||||
.getApi()
|
||||
.read(JsonRpcRequest("net_peerCount", listOf()))
|
||||
.flatMap(JsonRpcResponse::requireStringResult)
|
||||
.map(Integer::decode)
|
||||
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers")))
|
||||
.map { count ->
|
||||
val minPeers = options.minPeers ?: 1
|
||||
if (count < minPeers) {
|
||||
UpstreamAvailability.IMMATURE
|
||||
} else {
|
||||
UpstreamAvailability.OK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.single()
|
||||
.onErrorReturn(UpstreamAvailability.UNAVAILABLE)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -16,15 +16,13 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.ethereum
|
||||
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.Disposable
|
||||
|
||||
class EthereumWsHead(
|
||||
private val ws: EthereumWs
|
||||
): DefaultEthereumHead(), Lifecycle, CachesEnabled {
|
||||
private val ws: EthereumWsFactory.EthereumWs
|
||||
) : DefaultEthereumHead(), Lifecycle {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EthereumWsHead::class.java)
|
||||
|
||||
@@ -43,8 +41,4 @@ class EthereumWsHead(
|
||||
subscription = null
|
||||
}
|
||||
|
||||
override fun setCaches(caches: Caches) {
|
||||
ws.setCaches(caches)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,22 +22,21 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.data.BlockId
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.*
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.rpc.*
|
||||
import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.Lifecycle
|
||||
import reactor.core.Disposable
|
||||
@@ -58,16 +57,15 @@ open class EthereumGrpcUpstream(
|
||||
private val chain: Chain,
|
||||
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val rpcClient: ReactorEmeraldClient
|
||||
) : DefaultUpstream<EthereumApi>(
|
||||
private val client: JsonRpcGrpcClient
|
||||
) : DefaultUpstream(
|
||||
"$parentId/${chain.chainCode}",
|
||||
UpstreamsConfig.Options.getDefaults(),
|
||||
null
|
||||
), CachesEnabled, Lifecycle {
|
||||
), Lifecycle {
|
||||
|
||||
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
|
||||
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
|
||||
private var caches: Caches? = null
|
||||
|
||||
private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels())
|
||||
private val head = DefaultEthereumHead()
|
||||
@@ -76,16 +74,7 @@ open class EthereumGrpcUpstream(
|
||||
|
||||
var timeout = Defaults.timeout
|
||||
|
||||
open fun createApi(matcher: Selector.Matcher): DirectEthereumApi {
|
||||
val targets = this.getMethods()
|
||||
val client = Selector.extractLabels(matcher)?.let { selector ->
|
||||
rpcClient.copyWithSelector(selector.asProto())
|
||||
} ?: rpcClient
|
||||
return DirectEthereumApi(client, caches, objectMapper, targets).let {
|
||||
it.upstream = this
|
||||
it
|
||||
}
|
||||
}
|
||||
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
|
||||
|
||||
override fun start() {
|
||||
if (this.isRunning) return
|
||||
@@ -123,6 +112,7 @@ open class EthereumGrpcUpstream(
|
||||
BigInteger(1, value.weight.toByteArray()),
|
||||
Instant.ofEpochMilli(value.timestamp),
|
||||
false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
block
|
||||
@@ -132,9 +122,11 @@ open class EthereumGrpcUpstream(
|
||||
val curr = head.getCurrent()
|
||||
curr == null || curr.difficulty < block.difficulty
|
||||
}.flatMap {
|
||||
getApi(Selector.EmptyMatcher())
|
||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(BlockHash(it.hash.value))) }
|
||||
.map { BlockContainer.from(it, objectMapper) }
|
||||
defaultReader.read(JsonRpcRequest("eth_getBlockByHash", listOf(it.hash.toHexWithPrefix(), false)))
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map {
|
||||
BlockContainer.from(it, objectMapper)
|
||||
}
|
||||
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
|
||||
.doOnError { t ->
|
||||
setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||
@@ -208,26 +200,16 @@ open class EthereumGrpcUpstream(
|
||||
return head
|
||||
}
|
||||
|
||||
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> {
|
||||
return Mono.just(createApi(matcher))
|
||||
}
|
||||
|
||||
override fun setCaches(caches: Caches) {
|
||||
this.caches = caches
|
||||
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
return defaultReader
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
||||
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||
}
|
||||
return castApi(apiType) as T
|
||||
return this as T
|
||||
}
|
||||
|
||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
||||
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
|
||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
||||
}
|
||||
return this as Upstream<A>
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,10 @@ import io.emeraldpay.dshackle.FileResolver
|
||||
import io.emeraldpay.dshackle.config.AuthConfig
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.grpc.ManagedChannelBuilder
|
||||
import io.grpc.netty.NettyChannelBuilder
|
||||
import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient
|
||||
import io.netty.handler.ssl.*
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
@@ -57,7 +57,6 @@ class GrpcUpstreams(
|
||||
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
||||
private val known = HashMap<Chain, EthereumGrpcUpstream>()
|
||||
private val lock = ReentrantLock()
|
||||
private var grpcTransport: ReactorEmeraldClient? = null
|
||||
|
||||
fun start(): Flux<UpstreamChange> {
|
||||
val channel: ManagedChannelBuilder<*> = if (auth != null && StringUtils.isNotEmpty(auth.ca)) {
|
||||
@@ -74,10 +73,6 @@ class GrpcUpstreams(
|
||||
|
||||
val client = ReactorBlockchainGrpc.newReactorStub(channel.build())
|
||||
this.client = client
|
||||
this.grpcTransport = ReactorEmeraldClient.newBuilder()
|
||||
.connectUsing(client.channel)
|
||||
.objectMapper(objectMapper)
|
||||
.build()
|
||||
|
||||
val statusSubscription = AtomicReference<Disposable>()
|
||||
|
||||
@@ -162,7 +157,8 @@ class GrpcUpstreams(
|
||||
lock.withLock {
|
||||
val current = known[chain]
|
||||
return if (current == null) {
|
||||
val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
|
||||
val rpcClient = JsonRpcGrpcClient(client!!, chain, objectMapper)
|
||||
val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, rpcClient)
|
||||
created.timeout = this.timeout
|
||||
known[chain] = created
|
||||
created.start()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,27 +13,41 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
package io.emeraldpay.dshackle.upstream.rpcclient
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.config.AuthConfig
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.handler.codec.http.HttpHeaderNames
|
||||
import io.netty.handler.codec.http.HttpHeaders
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.netty.http.client.HttpClient
|
||||
import reactor.netty.tcp.SslProvider
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
|
||||
open class BitcoinRpcClient(
|
||||
/**
|
||||
* JSON RPC client
|
||||
*/
|
||||
class JsonRpcHttpClient(
|
||||
private val target: String,
|
||||
basicAuth: AuthConfig.ClientBasicAuth?
|
||||
) {
|
||||
private val objectMapper: ObjectMapper,
|
||||
basicAuth: AuthConfig.ClientBasicAuth? = null,
|
||||
tlsCAAuth: ByteArray? = null
|
||||
) : Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BitcoinRpcClient::class.java)
|
||||
private val log = LoggerFactory.getLogger(JsonRpcHttpClient::class.java)
|
||||
}
|
||||
|
||||
private val parser = JsonRpcParser()
|
||||
private val httpClient: HttpClient
|
||||
|
||||
init {
|
||||
@@ -43,14 +57,27 @@ open class BitcoinRpcClient(
|
||||
h.add(HttpHeaderNames.CONTENT_TYPE, "application/json")
|
||||
}
|
||||
|
||||
basicAuth?.let { basicAuth ->
|
||||
val authString: String = basicAuth.username + ":" + basicAuth.password
|
||||
basicAuth?.let { auth ->
|
||||
val authString: String = auth.username + ":" + auth.password
|
||||
val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray())
|
||||
val auth = "Basic $authBase64"
|
||||
val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, auth) }
|
||||
val encodedAuth = "Basic $authBase64"
|
||||
val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, encodedAuth) }
|
||||
build = build.headers(headers)
|
||||
}
|
||||
|
||||
tlsCAAuth?.let { auth ->
|
||||
val cf = CertificateFactory.getInstance("X.509")
|
||||
val cert = cf.generateCertificate(ByteArrayInputStream(auth)) as X509Certificate
|
||||
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
ks.load(null, "".toCharArray())
|
||||
ks.setCertificateEntry("server", cert)
|
||||
val sslContext = SslContextBuilder.forClient().trustManager(cert).build()
|
||||
|
||||
build.secure { spec ->
|
||||
spec.sslContext(sslContext)
|
||||
}
|
||||
}
|
||||
|
||||
this.httpClient = build
|
||||
}
|
||||
|
||||
@@ -65,5 +92,10 @@ open class BitcoinRpcClient(
|
||||
.asByteArray()
|
||||
}
|
||||
|
||||
|
||||
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
|
||||
return Mono.just(key)
|
||||
.map { it.toJson(objectMapper) }
|
||||
.flatMap(this@JsonRpcHttpClient::execute)
|
||||
.map(parser::parse)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ class BlocksRedisCacheSpec extends Specification {
|
||||
Instant.ofEpochSecond(10501050),
|
||||
false,
|
||||
"test".bytes,
|
||||
null,
|
||||
[TxId.from(hash2), TxId.from(hash1)]
|
||||
)
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ class TxRedisCacheSpec extends Specification {
|
||||
2000,
|
||||
TxId.from(hash1),
|
||||
BlockId.from(hash2),
|
||||
"test".bytes
|
||||
"test".bytes,
|
||||
null
|
||||
)
|
||||
when:
|
||||
def enc = cache.toProto(cont)
|
||||
|
||||
@@ -20,16 +20,16 @@ import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import spock.lang.Specification
|
||||
|
||||
class BroadcastQuorumSpec extends Specification {
|
||||
|
||||
def rpcConverted = TestingCommons.rpcConverter()
|
||||
def objectMapper = TestingCommons.objectMapper()
|
||||
|
||||
def "Resolved with first after 3 tries"() {
|
||||
setup:
|
||||
def q = Spy(new BroadcastQuorum(rpcConverted, 3))
|
||||
def q = Spy(new BroadcastQuorum(objectMapper, 3))
|
||||
def upstream1 = Stub(Upstream)
|
||||
def upstream2 = Stub(Upstream)
|
||||
def upstream3 = Stub(Upstream)
|
||||
@@ -40,28 +40,28 @@ class BroadcastQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream1)
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2)
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3)
|
||||
q.record(new RpcException(1, "Nonce too low"), upstream3)
|
||||
then:
|
||||
1 * q.recordError(_, _, _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]
|
||||
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||
}
|
||||
|
||||
def "Remembers first response"() {
|
||||
setup:
|
||||
def q = Spy(new BroadcastQuorum(rpcConverted, 3))
|
||||
def q = Spy(new BroadcastQuorum(objectMapper, 3))
|
||||
def upstream1 = Stub(Upstream)
|
||||
def upstream2 = Stub(Upstream)
|
||||
def upstream3 = Stub(Upstream)
|
||||
@@ -72,22 +72,22 @@ class BroadcastQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([error: [message: "Internal error"]]), upstream1)
|
||||
q.record(new RpcException(1, "Internal error"), upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordError(_, _, _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2)
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3)
|
||||
q.record(new RpcException(1, "Nonce too low"), upstream3)
|
||||
then:
|
||||
1 * q.recordError(_, _, _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]
|
||||
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -20,16 +20,16 @@ import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.quorum.NonceQuorum
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import spock.lang.Specification
|
||||
|
||||
class NonceQuorumSpec extends Specification {
|
||||
|
||||
def rpcConverted = TestingCommons.rpcConverter()
|
||||
def objectMapper = TestingCommons.objectMapper()
|
||||
|
||||
def "Gets max value"() {
|
||||
setup:
|
||||
def q = Spy(new NonceQuorum(rpcConverted, 3))
|
||||
def q = Spy(new NonceQuorum(objectMapper, 3))
|
||||
def upstream1 = Stub(Upstream)
|
||||
def upstream2 = Stub(Upstream)
|
||||
def upstream3 = Stub(Upstream)
|
||||
@@ -40,28 +40,28 @@ class NonceQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream1)
|
||||
q.record('"0x10"'.bytes, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0x10", _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2)
|
||||
q.record('"0x11"'.bytes, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0x11", _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3)
|
||||
q.record('"0x10"'.bytes, upstream3)
|
||||
then:
|
||||
1 * q.recordValue(_, "0x10", _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Map) == [result: "0x11"]
|
||||
objectMapper.readValue(q.result, Object) == "0x11"
|
||||
}
|
||||
|
||||
def "Ignores errors"() {
|
||||
setup:
|
||||
def q = Spy(new NonceQuorum(rpcConverted, 3))
|
||||
def q = Spy(new NonceQuorum(objectMapper, 3))
|
||||
def upstream1 = Stub(Upstream)
|
||||
def upstream2 = Stub(Upstream)
|
||||
def upstream3 = Stub(Upstream)
|
||||
@@ -72,28 +72,60 @@ class NonceQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([error: [error: "Internal"]]), upstream1)
|
||||
q.record(new RpcException(1, "Internal"), upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordError(_, _, _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2)
|
||||
q.record('"0x11"'.bytes, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0x11", _)
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3)
|
||||
q.record('"0x10"'.bytes, upstream3)
|
||||
then:
|
||||
1 * q.recordValue(_, "0x10", _)
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream1)
|
||||
q.record('"0x11"'.bytes, upstream1)
|
||||
then:
|
||||
1 * q.recordValue(_, "0x11", _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Map) == [result: "0x11"]
|
||||
objectMapper.readValue(q.result, Object) == "0x11"
|
||||
}
|
||||
|
||||
def "Fail if too many errors"() {
|
||||
setup:
|
||||
def q = Spy(new NonceQuorum(objectMapper, 3))
|
||||
def upstream1 = Stub(Upstream)
|
||||
def upstream2 = Stub(Upstream)
|
||||
def upstream3 = Stub(Upstream)
|
||||
|
||||
when:
|
||||
q.init(Stub(Head))
|
||||
then:
|
||||
!q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new RpcException(1, "Internal"), upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new RpcException(1, "Internal"), upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new RpcException(1, "Internal"), upstream3)
|
||||
then:
|
||||
q.isFailed()
|
||||
!q.isResolved()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,21 +19,25 @@ package io.emeraldpay.dshackle.rpc
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
|
||||
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
|
||||
import io.emeraldpay.dshackle.quorum.QuorumRpcReader
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
|
||||
import io.emeraldpay.dshackle.upstream.CachingEthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
|
||||
import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.NativeCallRouter
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
||||
import io.infinitape.etherjar.rpc.RpcException
|
||||
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||
import io.infinitape.etherjar.rpc.RpcResponseException
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.test.StepVerifier
|
||||
import spock.lang.Ignore
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
@@ -43,140 +47,99 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def objectMapper = TestingCommons.objectMapper()
|
||||
|
||||
def "Tries router first"() {
|
||||
def routedApi = Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null))
|
||||
}
|
||||
def upstream = Mock(Multistream) {
|
||||
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
||||
}
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def ctx = new NativeCall.CallContext<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"() {
|
||||
setup:
|
||||
def quorum = Spy(new AlwaysQuorum())
|
||||
def upstreams = Stub(Upstreams)
|
||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
||||
def apiMock = TestingCommons.api(rpcClient)
|
||||
apiMock.upstream = Stub(Upstream)
|
||||
def quorum = new AlwaysQuorum()
|
||||
|
||||
apiMock.answer("eth_test", [], "foo")
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||
Selector.empty, quorum,
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper())
|
||||
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, 1))
|
||||
}
|
||||
}
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
when:
|
||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||
def act = objectMapper.readValue(resp.payload, Map)
|
||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))
|
||||
def act = objectMapper.readValue(resp.payload, Object)
|
||||
then:
|
||||
act == [jsonrpc:"2.0", id:1, result: "foo"]
|
||||
1 * quorum.record(_, _)
|
||||
1 * quorum.getResult()
|
||||
}
|
||||
|
||||
def "Quorum may return not first received value"() {
|
||||
setup:
|
||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
||||
|
||||
def upstreams = Stub(Upstreams)
|
||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
||||
def apiMock = TestingCommons.api(rpcClient)
|
||||
apiMock.upstream = Stub(Upstream)
|
||||
|
||||
apiMock.answerOnce("eth_test", [], null)
|
||||
apiMock.answerOnce("eth_test", [], "bar")
|
||||
apiMock.answerOnce("eth_test", [], null)
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||
Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
|
||||
when:
|
||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||
def act = objectMapper.readValue(resp.payload, Map)
|
||||
then:
|
||||
act == [jsonrpc:"2.0", id:1, result: "bar"]
|
||||
2 * quorum.record(_, _)
|
||||
1 * quorum.getResult()
|
||||
}
|
||||
|
||||
def "Have pause between repeats"() {
|
||||
setup:
|
||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
||||
|
||||
def upstreams = Stub(Upstreams)
|
||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
||||
def apiMock = TestingCommons.api(rpcClient)
|
||||
apiMock.upstream = Stub(Upstream)
|
||||
|
||||
apiMock.answerOnce("eth_test", [], null)
|
||||
apiMock.answerOnce("eth_test", [], "bar")
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||
Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
|
||||
when:
|
||||
def t1 = System.currentTimeMillis()
|
||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||
def delta = System.currentTimeMillis() - t1
|
||||
then:
|
||||
delta > 95 // should be 100, but sometimes gives less ???
|
||||
new String(resp.payload) == '{"jsonrpc":"2.0","id":1,"result":"bar"}'
|
||||
}
|
||||
|
||||
def "One call has no pause"() {
|
||||
setup:
|
||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
||||
|
||||
def upstreams = Stub(Upstreams)
|
||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
||||
def apiMock = TestingCommons.api(rpcClient)
|
||||
apiMock.upstream = Stub(Upstream)
|
||||
|
||||
apiMock.answerOnce("eth_test", [], "bar")
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||
Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
|
||||
when:
|
||||
def t1 = System.currentTimeMillis()
|
||||
nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||
def delta = System.currentTimeMillis() - t1
|
||||
then:
|
||||
delta < 50
|
||||
act == "foo"
|
||||
}
|
||||
|
||||
def "Returns error if no quorum"() {
|
||||
setup:
|
||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
||||
def quorum = new AlwaysQuorum()
|
||||
|
||||
def upstreams = Stub(Upstreams)
|
||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
||||
def apiMock = TestingCommons.api(rpcClient)
|
||||
apiMock.upstream = Stub(Upstream)
|
||||
|
||||
apiMock.answer("eth_test", [], null, 3)
|
||||
apiMock.answerOnce("eth_test", [], "foo")
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), Selector.empty, quorum,
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper())
|
||||
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * read(_) >> Mono.empty()
|
||||
}
|
||||
}
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
3 * quorum.record(_, _)
|
||||
1 * quorum.getResult()
|
||||
|
||||
when:
|
||||
def resp = nativeCall.executeOnRemote(call)
|
||||
then:
|
||||
StepVerifier.create(resp)
|
||||
.expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 1})
|
||||
.verify(Duration.ofSeconds(1))
|
||||
.expectErrorMatches({ t -> t instanceof NativeCall.CallFailure && t.id == 1 })
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Packs call exception into response with id"() {
|
||||
setup:
|
||||
def upstreams = Stub(Upstreams)
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
when:
|
||||
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
|
||||
@@ -193,7 +156,7 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Packs unknown exception into response"() {
|
||||
setup:
|
||||
def upstreams = Stub(Upstreams)
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
when:
|
||||
def resp = nativeCall.processException(new IllegalArgumentException("test test"))
|
||||
@@ -209,13 +172,13 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Builds normal response"() {
|
||||
setup:
|
||||
def upstreams = Stub(Upstreams)
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def json = [jsonrpc:"2.0", id:1, result: "foo"]
|
||||
|
||||
when:
|
||||
def resp = nativeCall.buildResponse(
|
||||
new NativeCall.CallContext<byte[]>(1561, TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi)), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json))
|
||||
new NativeCall.CallContext<byte[]>(1561, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json))
|
||||
)
|
||||
then:
|
||||
resp.id == 1561
|
||||
@@ -225,7 +188,7 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Returns error for invalid chain"() {
|
||||
setup:
|
||||
def upstreams = Stub(Upstreams)
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
@@ -248,7 +211,7 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Returns error for unsupported chain"() {
|
||||
setup:
|
||||
def upstreams = Mock(Upstreams)
|
||||
def upstreams = Mock(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
@@ -270,14 +233,14 @@ class NativeCallSpec extends Specification {
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
@Ignore
|
||||
//TODO
|
||||
def "Calls cache before remote"() {
|
||||
setup:
|
||||
def upstreams = Stub(Upstreams)
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def api = Mock(DirectEthereumApi)
|
||||
def api = TestingCommons.api()
|
||||
def upstream = TestingCommons.aggregatedUpstream(api)
|
||||
def cacheMock = Mock(CachingEthereumApi)
|
||||
upstream.cache = cacheMock
|
||||
|
||||
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
|
||||
upstream,
|
||||
@@ -289,13 +252,13 @@ class NativeCallSpec extends Specification {
|
||||
1 * cacheMock.execute(10, "eth_test", []) >> Mono.empty()
|
||||
}
|
||||
|
||||
@Ignore
|
||||
//TODO
|
||||
def "Uses cached value"() {
|
||||
setup:
|
||||
def upstreams = Stub(Upstreams)
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def upstream = TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi))
|
||||
def cacheMock = Mock(CachingEthereumApi)
|
||||
upstream.cache = cacheMock
|
||||
def upstream = TestingCommons.aggregatedUpstream(TestingCommons.api())
|
||||
|
||||
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
|
||||
upstream,
|
||||
@@ -307,63 +270,4 @@ class NativeCallSpec extends Specification {
|
||||
1 * cacheMock.execute(10, "eth_test", []) >> Mono.just('{"result": "foo"}'.bytes)
|
||||
new String(act.block().payload) == '{"result": "foo"}'
|
||||
}
|
||||
|
||||
def "Retries on error"() {
|
||||
setup:
|
||||
def quorum = Spy(new AlwaysQuorum())
|
||||
|
||||
def upstreams = Stub(Upstreams)
|
||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
||||
def apiMock = TestingCommons.api(rpcClient)
|
||||
apiMock.upstream = Stub(Upstream)
|
||||
|
||||
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 1"))
|
||||
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 2"))
|
||||
apiMock.answerOnce("eth_test", [], "bar")
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||
Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
|
||||
when:
|
||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||
def act = objectMapper.readValue(resp.payload, Map)
|
||||
then:
|
||||
act == [jsonrpc:"2.0", id:1, result: "bar"]
|
||||
1 * quorum.record(_, _)
|
||||
1 * quorum.getResult()
|
||||
}
|
||||
|
||||
def "Send raw retries 3 times"() {
|
||||
setup:
|
||||
def quorum = Spy(new BroadcastQuorum(TestingCommons.rpcConverter(), 3))
|
||||
|
||||
def upstreams = Stub(Upstreams)
|
||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
||||
def apiMock = TestingCommons.api(rpcClient)
|
||||
apiMock.upstream = Stub(Upstream)
|
||||
|
||||
apiMock.answer("eth_sendRawTransaction", ["0x1234"],
|
||||
"0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b", 1)
|
||||
apiMock.answer("eth_sendRawTransaction", ["0x1234"], null, 10,
|
||||
new RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported"))
|
||||
// apiMock.answer("eth_sendRawTransaction", ["0x1234"],
|
||||
// new RpcResponseError(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported"), 10)
|
||||
|
||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||
Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_sendRawTransaction", ["0x1234"]))
|
||||
|
||||
|
||||
when:
|
||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||
def act = objectMapper.readValue(resp.payload, Map)
|
||||
then:
|
||||
act == [jsonrpc:"2.0", id:1, result: "0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b"]
|
||||
1 * quorum.record(_ as byte[], _)
|
||||
2 * quorum.record(_ as RpcException, _)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,10 @@ import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.test.EthereumUpstreamMock
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.test.UpstreamsMock
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||
import reactor.core.publisher.Mono
|
||||
@@ -45,7 +42,7 @@ class StreamHeadSpec extends Specification {
|
||||
|
||||
def "Errors on unavailable chain"() {
|
||||
setup:
|
||||
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(EthereumUpstream))
|
||||
def upstreams = new MultistreamHolderMock(Chain.ETHEREUM, Stub(EthereumUpstream))
|
||||
def streamHead = new StreamHead(upstreams)
|
||||
when:
|
||||
def flux = streamHead.add(
|
||||
@@ -80,8 +77,8 @@ class StreamHeadSpec extends Specification {
|
||||
.build()
|
||||
}
|
||||
|
||||
def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, Stub(DirectEthereumApi.class))
|
||||
def upstreams = new UpstreamsMock(Chain.ETHEREUM, upstream)
|
||||
def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, TestingCommons.api())
|
||||
def upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstream)
|
||||
def streamHead = new StreamHead(upstreams)
|
||||
when:
|
||||
def flux = streamHead.add(
|
||||
|
||||
@@ -20,11 +20,11 @@ import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.data.BlockId
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
||||
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
@@ -43,7 +43,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json")
|
||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
||||
|
||||
@@ -58,7 +58,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
||||
|
||||
@@ -73,7 +73,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address }
|
||||
|
||||
@@ -93,7 +93,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
|
||||
def "Zero for empty unspents"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], [])
|
||||
|
||||
@@ -108,7 +108,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address }
|
||||
|
||||
@@ -126,7 +126,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
|
||||
def "One address for single provided"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||
.setAddress(
|
||||
Common.AnyAddress.newBuilder()
|
||||
@@ -144,7 +144,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
|
||||
def "Sorted addresses for multiple provided"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||
.setAddress(
|
||||
Common.AnyAddress.newBuilder()
|
||||
@@ -165,7 +165,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
|
||||
def "Null for no address provided"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||
.build()
|
||||
when:
|
||||
@@ -176,7 +176,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
|
||||
def "Build proto for common balance"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(123456))
|
||||
when:
|
||||
def act = track.buildResponse(balance)
|
||||
@@ -189,7 +189,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
|
||||
def "Build proto for zero balance"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.ZERO)
|
||||
when:
|
||||
def act = track.buildResponse(balance)
|
||||
@@ -202,7 +202,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
|
||||
def "Build proto for all bitcoins"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(21_000_000).multiply(BigInteger.TEN.pow(8)))
|
||||
when:
|
||||
def act = track.buildResponse(balance)
|
||||
@@ -216,24 +216,24 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
def "Get update for a balance"() {
|
||||
setup:
|
||||
|
||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
||||
2 * executeAndResult(0, "listunspent", [], List) >>> [
|
||||
Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
|
||||
]
|
||||
}
|
||||
def blocks = TopicProcessor.create()
|
||||
Head head = Mock(Head) {
|
||||
1 * getFlux() >> Flux.from(blocks)
|
||||
}
|
||||
Upstream upstream
|
||||
upstream = Mock(AggregatedUpstream) {
|
||||
_ * getApi(_) >> Mono.just(api)
|
||||
def upstream = null
|
||||
upstream = Mock(BitcoinMultistream) {
|
||||
_ * getReader() >> Mock(BitcoinReader) {
|
||||
2 * listUnspent() >>> [
|
||||
Mono.just([]),
|
||||
Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
|
||||
]
|
||||
}
|
||||
_ * getHead() >> head
|
||||
_ * castApi(_) >> { return upstream }
|
||||
}
|
||||
Upstreams upstreams = Mock(Upstreams) {
|
||||
_ * getUpstream(Chain.BITCOIN) >> upstream
|
||||
_ * cast(_) >> {
|
||||
upstream
|
||||
}
|
||||
}
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.BITCOIN, upstream)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams)
|
||||
|
||||
when:
|
||||
@@ -253,7 +253,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
StepVerifier.create(resp)
|
||||
.expectNext("0")
|
||||
.then {
|
||||
blocks.onNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, []))
|
||||
blocks.onNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, null, []))
|
||||
}
|
||||
.expectNext("1230000")
|
||||
.then {
|
||||
|
||||
@@ -18,11 +18,9 @@ package io.emeraldpay.dshackle.rpc
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.data.BlockId
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.CachingMempoolData
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import reactor.core.publisher.Flux
|
||||
@@ -37,7 +35,7 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "loadMempool() returns not found when not found"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
|
||||
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
|
||||
1 * get() >> Mono.just([
|
||||
@@ -45,8 +43,8 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
|
||||
])
|
||||
}
|
||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
||||
_ * getData() >> Mock(BitcoinReader) {
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
_ * getReader() >> Mock(BitcoinReader) {
|
||||
_ * getMempool() >> mempoolAccess
|
||||
}
|
||||
}
|
||||
@@ -64,15 +62,15 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "loadMempool() returns ok when found"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
|
||||
1 * get() >> Mono.just([
|
||||
"69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9",
|
||||
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
|
||||
])
|
||||
}
|
||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
||||
_ * getData() >> Mock(BitcoinReader) {
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
_ * getReader() >> Mock(BitcoinReader) {
|
||||
_ * getMempool() >> mempoolAccess
|
||||
}
|
||||
}
|
||||
@@ -90,15 +88,17 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "loadExiting() returns not found if not mined"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
||||
1 * getTx(txid) >> Mono.just([
|
||||
txid: txid
|
||||
])
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
_ * getReader() >> Mock(BitcoinReader) {
|
||||
1 * getTx(txid) >> Mono.just([
|
||||
txid: txid
|
||||
])
|
||||
}
|
||||
}
|
||||
when:
|
||||
def act = track.loadExisting(api, txid)
|
||||
def act = track.loadExisting(upstream, txid)
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
@@ -111,17 +111,19 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "loadExiting() returns block if mined"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
||||
1 * getTx(txid) >> Mono.just([
|
||||
txid : txid,
|
||||
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
|
||||
height : 100
|
||||
])
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
_ * getReader() >> Mock(BitcoinReader) {
|
||||
1 * getTx(txid) >> Mono.just([
|
||||
txid : txid,
|
||||
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
|
||||
height : 100
|
||||
])
|
||||
}
|
||||
}
|
||||
when:
|
||||
def act = track.loadExisting(api, txid)
|
||||
def act = track.loadExisting(upstream, txid)
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
@@ -136,16 +138,16 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "Goes with confirmations"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||
// start with the current block
|
||||
def next = Flux.fromIterable([10, 12, 13, 14, 15]).map { h ->
|
||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, [])
|
||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
|
||||
}
|
||||
Head head = Mock(Head) {
|
||||
1 * getFlux() >> next
|
||||
}
|
||||
Upstream upstream = Mock(BitcoinUpstream) {
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
1 * getHead() >> head
|
||||
}
|
||||
def status = new TrackBitcoinTx.TxStatus(
|
||||
@@ -167,16 +169,16 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "Wait until mined"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||
// start with the current block
|
||||
def next = Flux.fromIterable([10, 12, 13]).map { h ->
|
||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, [])
|
||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
|
||||
}
|
||||
Head head = Mock(Head) {
|
||||
1 * getFlux() >> next
|
||||
}
|
||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
||||
BitcoinReader api = Mock(BitcoinReader) {
|
||||
3 * getTx(txid) >>> [
|
||||
Mono.just([
|
||||
txid: txid
|
||||
@@ -191,9 +193,9 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
])
|
||||
]
|
||||
}
|
||||
Upstream upstream = Mock(BitcoinUpstream) {
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
1 * getHead() >> head
|
||||
_ * getApi(_) >> Mono.just(api)
|
||||
_ * getReader() >> api
|
||||
}
|
||||
def status = new TrackBitcoinTx.TxStatus(
|
||||
txid, false, null, false, null, null, null, 0
|
||||
@@ -210,13 +212,9 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "Check mempool until found"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
||||
1 * getTx(txid) >> Mono.just([
|
||||
txid: txid
|
||||
])
|
||||
}
|
||||
|
||||
Head head = Mock(Head) {
|
||||
_ * getFlux() >> Flux.empty()
|
||||
}
|
||||
@@ -228,17 +226,20 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over
|
||||
]
|
||||
}
|
||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
||||
_ * getApi(_) >> Mono.just(api)
|
||||
BitcoinReader api = Mock(BitcoinReader) {
|
||||
1 * getTx(txid) >> Mono.just([
|
||||
txid: txid
|
||||
])
|
||||
_ * getMempool() >> mempoolAccess
|
||||
}
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
_ * getHead() >> head
|
||||
_ * getData() >> Mock(BitcoinReader) {
|
||||
_ * getMempool() >> mempoolAccess
|
||||
}
|
||||
_ * getReader() >> api
|
||||
}
|
||||
|
||||
when:
|
||||
def steps = StepVerifier.withVirtualTime {
|
||||
track.untilFound(Chain.BITCOIN, api, upstream, txid).take(1)
|
||||
track.untilFound(Chain.BITCOIN, upstream, txid).take(1)
|
||||
}
|
||||
|
||||
then:
|
||||
@@ -252,9 +253,9 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
|
||||
def "Subscribe to an existing tx"() {
|
||||
setup:
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
||||
BitcoinReader api = Mock(BitcoinReader) {
|
||||
_ * getTx(txid) >> Mono.just([
|
||||
txid : txid,
|
||||
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
|
||||
@@ -267,18 +268,18 @@ class TrackBitcoinTxSpec extends Specification {
|
||||
])
|
||||
}
|
||||
def next = Flux.fromIterable([10, 11, 12]).map { h ->
|
||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, [])
|
||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
|
||||
}
|
||||
Head head = Mock(Head) {
|
||||
_ * getFlux() >> next
|
||||
}
|
||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
||||
_ * getApi(_) >> Mono.just(api)
|
||||
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||
_ * getReader() >> api
|
||||
_ * getHead() >> head
|
||||
}
|
||||
|
||||
when:
|
||||
def act = track.subscribe(Chain.BITCOIN, api, upstream, txid)
|
||||
def act = track.subscribe(Chain.BITCOIN, upstream, txid)
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
|
||||
@@ -19,21 +19,12 @@ package io.emeraldpay.dshackle.rpc
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.test.UpstreamsMock
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
|
||||
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.domain.Address
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
||||
import io.infinitape.etherjar.rpc.RpcCall
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.core.publisher.TopicProcessor
|
||||
import reactor.core.scheduler.Schedulers
|
||||
import reactor.test.StepVerifier
|
||||
import spock.lang.Specification
|
||||
|
||||
@@ -64,9 +55,9 @@ class TrackEthereumAddressSpec extends Specification {
|
||||
.setBalance("1234567890")
|
||||
.build()
|
||||
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
|
||||
|
||||
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||
@@ -104,9 +95,9 @@ class TrackEthereumAddressSpec extends Specification {
|
||||
return it
|
||||
}
|
||||
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
|
||||
|
||||
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||
@@ -121,6 +112,6 @@ class TrackEthereumAddressSpec extends Specification {
|
||||
}
|
||||
.expectNext(exp2).as("Second block")
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
.verify(Duration.ofSeconds(2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,15 +23,13 @@ import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.data.BlockId
|
||||
import io.emeraldpay.dshackle.data.TxId
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.test.UpstreamsMock
|
||||
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.domain.BlockHash
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||
@@ -98,9 +96,9 @@ class TrackEthereumTxSpec extends Specification {
|
||||
.setTimestamp(blockJson.timestamp.toEpochMilli())
|
||||
).build()
|
||||
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||
|
||||
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
|
||||
@@ -118,10 +116,10 @@ class TrackEthereumTxSpec extends Specification {
|
||||
|
||||
def "Wait for unknown transaction"() {
|
||||
setup:
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
((AggregatedEthereumUpstreams) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) {
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
((EthereumMultistream) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) {
|
||||
_ * getFlux() >> Flux.empty()
|
||||
}
|
||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||
@@ -133,7 +131,7 @@ class TrackEthereumTxSpec extends Specification {
|
||||
when:
|
||||
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
||||
def act = StepVerifier.withVirtualTime(
|
||||
{ trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).castApi(EthereumApi.class)) },
|
||||
{ trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).cast(EthereumMultistream)) },
|
||||
{ scheduler },
|
||||
5)
|
||||
|
||||
@@ -168,9 +166,9 @@ class TrackEthereumTxSpec extends Specification {
|
||||
it
|
||||
}
|
||||
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||
def scheduler = VirtualTimeScheduler.create(true)
|
||||
trackTx.scheduler = scheduler
|
||||
@@ -193,14 +191,14 @@ class TrackEthereumTxSpec extends Specification {
|
||||
|
||||
def "New block makes tx mined"() {
|
||||
setup:
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||
|
||||
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
||||
def block = new BlockContainer(
|
||||
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes,
|
||||
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null,
|
||||
[TxId.from(txId)]
|
||||
)
|
||||
|
||||
@@ -215,14 +213,14 @@ class TrackEthereumTxSpec extends Specification {
|
||||
|
||||
def "New block without current tx requires a call"() {
|
||||
setup:
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||
|
||||
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
||||
def block = new BlockContainer(
|
||||
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes,
|
||||
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null,
|
||||
[TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")]
|
||||
)
|
||||
apiMock.answer("eth_getTransactionByHash", [txId], null)
|
||||
@@ -288,9 +286,9 @@ class TrackEthereumTxSpec extends Specification {
|
||||
)
|
||||
|
||||
|
||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||
|
||||
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
|
||||
|
||||
@@ -19,11 +19,10 @@ package io.emeraldpay.dshackle.test
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.grpc.stub.StreamObserver
|
||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
||||
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||
import org.jetbrains.annotations.NotNull
|
||||
@@ -31,16 +30,18 @@ import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
class EthereumApiMock extends DirectEthereumApi {
|
||||
class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(this)
|
||||
List<PredefinedResponse> predefined = []
|
||||
private ObjectMapper objectMapper
|
||||
|
||||
EthereumApiMock(@NotNull ReactorRpcClient rpcClient, @NotNull ObjectMapper objectMapper, @NotNull Chain chain) {
|
||||
super(rpcClient, null, objectMapper, new DirectCallMethods())
|
||||
String id = "default"
|
||||
|
||||
EthereumApiMock(@NotNull ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper
|
||||
}
|
||||
|
||||
@@ -55,10 +56,11 @@ class EthereumApiMock extends DirectEthereumApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<byte[]> execute(int id, @NotNull String method, @NotNull List<?> params) {
|
||||
Callable<byte[]> call = {
|
||||
def predefined = predefined.find { it.isSame(id, method, params) }
|
||||
ResponseJson json = new ResponseJson<Object, Integer>(id: id)
|
||||
Mono<JsonRpcResponse> read(JsonRpcRequest request) {
|
||||
Callable<JsonRpcResponse> call = {
|
||||
def predefined = predefined.find { it.isSame(request.method, request.params) }
|
||||
byte[] result = null
|
||||
JsonRpcResponse.ResponseError error = null
|
||||
if (predefined != null) {
|
||||
if (predefined.exception != null) {
|
||||
predefined.onCalled()
|
||||
@@ -66,32 +68,37 @@ class EthereumApiMock extends DirectEthereumApi {
|
||||
throw predefined.exception
|
||||
}
|
||||
if (predefined.result instanceof RpcResponseError) {
|
||||
json.error = predefined.result
|
||||
((RpcResponseError) predefined.result).with { err ->
|
||||
error = new JsonRpcResponse.ResponseError(err.code, err.message)
|
||||
}
|
||||
} else {
|
||||
json.result = predefined.result
|
||||
// ResponseJson json = new ResponseJson<Object, Integer>(id: 1, result: predefined.result)
|
||||
result = objectMapper.writeValueAsBytes(predefined.result)
|
||||
}
|
||||
predefined.onCalled()
|
||||
predefined.print()
|
||||
} else {
|
||||
log.error("Method ${method} with ${params} is not mocked")
|
||||
json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked")
|
||||
log.error("Method ${request.method} with ${request.params} is not mocked")
|
||||
error = new JsonRpcResponse.ResponseError(-32601, "Method ${request.method} with ${request.params} is not mocked")
|
||||
}
|
||||
byte[] result = objectMapper.writeValueAsBytes(json)
|
||||
return result
|
||||
} as Callable<byte[]>
|
||||
return new JsonRpcResponse(result, error)
|
||||
} as Callable<JsonRpcResponse>
|
||||
return Mono.fromCallable(call)
|
||||
}
|
||||
|
||||
def nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
|
||||
request.itemsList.forEach { req ->
|
||||
def resp = execute(req.id, req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray()))
|
||||
resp.subscribe {
|
||||
def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
|
||||
.setId(req.id)
|
||||
.setSucceed(true)
|
||||
.setPayload(ByteString.copyFrom(resp.block()))
|
||||
responseObserver.onNext(proto.build())
|
||||
JsonRpcResponse resp = read(new JsonRpcRequest(req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray())))
|
||||
.block(Duration.ofSeconds(5))
|
||||
def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
|
||||
.setId(req.id)
|
||||
.setSucceed(resp.hasResult())
|
||||
.setPayload(ByteString.copyFrom(resp.getResult()))
|
||||
|
||||
resp.error?.with { err ->
|
||||
proto.setErrorMessage(err.message)
|
||||
}
|
||||
responseObserver.onNext(proto.build())
|
||||
}
|
||||
responseObserver.onCompleted()
|
||||
}
|
||||
@@ -103,7 +110,7 @@ class EthereumApiMock extends DirectEthereumApi {
|
||||
Integer limit
|
||||
Throwable exception
|
||||
|
||||
boolean isSame(int id, String method, List<?> params) {
|
||||
boolean isSame(String method, List<?> params) {
|
||||
if (limit != null) {
|
||||
if (limit <= 0) {
|
||||
return false
|
||||
|
||||
@@ -16,28 +16,20 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.test
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.infinitape.etherjar.rpc.ReactorBatch
|
||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
||||
import io.infinitape.etherjar.rpc.RpcCall
|
||||
import io.infinitape.etherjar.rpc.RpcCallResponse
|
||||
import reactor.core.publisher.Flux
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
class EthereumApiStub extends DirectEthereumApi {
|
||||
class EthereumApiStub implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
private String id
|
||||
private static ObjectMapper objectMapper = TestingCommons.objectMapper()
|
||||
private static ReactorRpcClient rpcClient = new RpcClientMock();
|
||||
|
||||
EthereumApiStub(Integer id) {
|
||||
this(id.toString())
|
||||
}
|
||||
|
||||
EthereumApiStub(String id) {
|
||||
super(rpcClient, null, objectMapper, new DirectCallMethods())
|
||||
this.id = id
|
||||
}
|
||||
|
||||
@@ -46,16 +38,9 @@ class EthereumApiStub extends DirectEthereumApi {
|
||||
return "API Stub $id"
|
||||
}
|
||||
|
||||
static class RpcClientMock implements ReactorRpcClient {
|
||||
|
||||
@Override
|
||||
Flux<RpcCallResponse> execute(ReactorBatch batch) {
|
||||
return Flux.error(new Exception("Not implemented in mock"))
|
||||
}
|
||||
|
||||
@Override
|
||||
def <JS, RES> Mono<RES> execute(RpcCall<JS, RES> call) {
|
||||
return Mono.error(new Exception("Not implemented in mock"))
|
||||
}
|
||||
@Override
|
||||
Mono<JsonRpcResponse> read(JsonRpcRequest key) {
|
||||
return Mono.error(new Exception("Not implemented in mock"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.test
|
||||
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import org.reactivestreams.Publisher
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
@@ -28,9 +29,14 @@ class EthereumHeadMock implements Head {
|
||||
private TopicProcessor<BlockContainer> bus = TopicProcessor.create()
|
||||
private Publisher<BlockContainer> predefined = null
|
||||
private BlockContainer latest
|
||||
private List<Runnable> handlers = []
|
||||
|
||||
void nextBlock(BlockContainer block) {
|
||||
handlers.forEach {
|
||||
it.run()
|
||||
}
|
||||
assert block != null
|
||||
println("New block: ${block.height} / ${block.hash}")
|
||||
latest = block
|
||||
bus.onNext(block)
|
||||
}
|
||||
@@ -51,4 +57,9 @@ class EthereumHeadMock implements Head {
|
||||
return Flux.concat(Mono.justOrEmpty(latest), bus).distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onBeforeBlock(@NotNull Runnable handler) {
|
||||
handlers.add(handler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,22 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.test
|
||||
|
||||
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import org.reactivestreams.Publisher
|
||||
|
||||
@@ -35,24 +39,33 @@ class EthereumUpstreamMock extends EthereumUpstream {
|
||||
|
||||
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
|
||||
|
||||
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
||||
this(chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
|
||||
static CallMethods allMethods() {
|
||||
new AggregatedCallMethods([
|
||||
new DefaultEthereumMethods(TestingCommons.objectMapper(), Chain.ETHEREUM),
|
||||
new DefaultBitcoinMethods(TestingCommons.objectMapper()),
|
||||
new DirectCallMethods(["eth_test"])
|
||||
])
|
||||
}
|
||||
|
||||
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
||||
this(id, chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
|
||||
EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
|
||||
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)
|
||||
}
|
||||
|
||||
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) {
|
||||
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
|
||||
super(id, chain, api, null,
|
||||
UpstreamsConfig.Options.getDefaults(), new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
|
||||
methods, TestingCommons.objectMapper())
|
||||
setLag(0)
|
||||
setStatus(UpstreamAvailability.OK)
|
||||
start()
|
||||
}
|
||||
|
||||
void nextBlock(BlockContainer block) {
|
||||
|
||||
@@ -16,48 +16,60 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.test
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
|
||||
import io.emeraldpay.dshackle.BlockchainType
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
||||
import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
class UpstreamsMock implements Upstreams {
|
||||
class MultistreamHolderMock implements MultistreamHolder {
|
||||
|
||||
private Map<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)
|
||||
}
|
||||
UpstreamsMock(Chain chain1, Upstream up1, Chain chain2, Upstream up2) {
|
||||
addUpstream(chain1, up1)
|
||||
addUpstream(chain2, up2)
|
||||
}
|
||||
|
||||
AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull EthereumUpstream up) {
|
||||
Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
|
||||
if (!upstreams.containsKey(chain)) {
|
||||
upstreams[chain] = new AggregatedEthereumUpstreamsMock(chain, [up], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper())
|
||||
upstreams[chain].start()
|
||||
if (BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM) {
|
||||
if (up instanceof EthereumMultistream) {
|
||||
upstreams[chain] = up
|
||||
} else if (up instanceof EthereumUpstream) {
|
||||
upstreams[chain] = new EthereumMultistreamMock(chain, [up as EthereumUpstream], Caches.default(TestingCommons.objectMapper()))
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
|
||||
}
|
||||
upstreams[chain].start()
|
||||
} else if (BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN) {
|
||||
if (up instanceof BitcoinMultistream) {
|
||||
upstreams[chain] = up
|
||||
} else if (up instanceof BitcoinUpstream) {
|
||||
upstreams[chain] = new BitcoinMultistream(chain, [up as BitcoinUpstream], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper())
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
|
||||
}
|
||||
upstreams[chain].start()
|
||||
}
|
||||
} else {
|
||||
upstreams[chain].addUpstream(up)
|
||||
}
|
||||
return upstreams[chain]
|
||||
}
|
||||
|
||||
void setReader(@NotNull Chain chain, EthereumReader reader) {
|
||||
upstreams[chain].customReader = reader
|
||||
}
|
||||
|
||||
@Override
|
||||
AggregatedUpstream getUpstream(@NotNull Chain chain) {
|
||||
Multistream getUpstream(@NotNull Chain chain) {
|
||||
return upstreams[chain]
|
||||
}
|
||||
|
||||
@@ -85,12 +97,12 @@ class UpstreamsMock implements Upstreams {
|
||||
return upstreams.containsKey(chain)
|
||||
}
|
||||
|
||||
static class AggregatedEthereumUpstreamsMock extends AggregatedEthereumUpstreams {
|
||||
static class EthereumMultistreamMock extends EthereumMultistream {
|
||||
|
||||
EthereumReader customReader = null
|
||||
|
||||
AggregatedEthereumUpstreamsMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches, @NotNull ObjectMapper objectMapper) {
|
||||
super(chain, upstreams, caches, objectMapper)
|
||||
EthereumMultistreamMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches) {
|
||||
super(chain, upstreams, caches, TestingCommons.objectMapper())
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,6 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 EmeraldPay, Inc
|
||||
* Copyright (c) 2020 ETCDEV GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,20 +13,25 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
package io.emeraldpay.dshackle.test
|
||||
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
/**
|
||||
* A general interface to make a request to an Upstream API
|
||||
*/
|
||||
interface UpstreamApi {
|
||||
class ReaderMock<K, D> implements Reader<K, D> {
|
||||
|
||||
/**
|
||||
* @param id an internal uniq id, if multiple requests are made in batch
|
||||
* @param method JSON RPC method name
|
||||
* @param params JSON RPC parameters, must be serializable into a JSON array
|
||||
*/
|
||||
fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
|
||||
private Map<K, D> mapping = new HashMap<K, D>()
|
||||
|
||||
}
|
||||
ReaderMock() {
|
||||
}
|
||||
|
||||
ReaderMock with(K key, D data) {
|
||||
mapping[key] = data
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<D> read(K key) {
|
||||
return Mono.justOrEmpty(mapping.get(key))
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,15 @@ import io.emeraldpay.dshackle.FileResolver
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||
import io.emeraldpay.dshackle.config.CacheConfig
|
||||
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@@ -50,32 +51,34 @@ class TestingCommons {
|
||||
return objectMapper
|
||||
}
|
||||
|
||||
static EthereumApiMock api(ReactorRpcClient rpcClient) {
|
||||
return new EthereumApiMock(rpcClient, objectMapper(), Chain.ETHEREUM)
|
||||
static EthereumApiMock api() {
|
||||
return new EthereumApiMock(objectMapper())
|
||||
}
|
||||
|
||||
static JacksonRpcConverter rpcConverter() {
|
||||
return new JacksonRpcConverter(objectMapper())
|
||||
}
|
||||
|
||||
static EthereumUpstreamMock upstream(DirectEthereumApi api) {
|
||||
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
|
||||
return new EthereumUpstreamMock(Chain.ETHEREUM, api)
|
||||
}
|
||||
|
||||
static EthereumUpstreamMock upstream(DirectEthereumApi api, String method) {
|
||||
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, String method) {
|
||||
return upstream(api, [method])
|
||||
}
|
||||
|
||||
static EthereumUpstreamMock upstream(DirectEthereumApi api, List<String> methods) {
|
||||
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, List<String> methods) {
|
||||
return new EthereumUpstreamMock(Chain.ETHEREUM, api, new DirectCallMethods(methods))
|
||||
}
|
||||
|
||||
static AggregatedUpstream aggregatedUpstream(DirectEthereumApi api) {
|
||||
static Multistream aggregatedUpstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
|
||||
return aggregatedUpstream(upstream(api))
|
||||
}
|
||||
|
||||
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) {
|
||||
return new AggregatedEthereumUpstreams(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper())
|
||||
static Multistream aggregatedUpstream(EthereumUpstream up) {
|
||||
return new EthereumMultistream(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper()).tap {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
static CachesFactory emptyCaches() {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user