problem: bitcoin mempool can be queries too often

solution: introduce data access wrappers, in this case caching mempool state in memory
This commit is contained in:
Igor Artamonov
2020-04-27 00:05:02 -04:00
parent c9082b3cd0
commit bd58ba9774
15 changed files with 197 additions and 73 deletions

View File

@@ -6,7 +6,7 @@ import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -48,7 +48,7 @@ class TrackBitcoinAddress(
} }
} }
fun requestBalances(chain: Chain, api: BitcoinApi, addresses: List<String>): Flux<AddressBalance> { fun requestBalances(chain: Chain, api: DirectBitcoinApi, addresses: List<String>): Flux<AddressBalance> {
return api.executeAndResult(0, "listunspent", emptyList(), List::class.java) return api.executeAndResult(0, "listunspent", emptyList(), List::class.java)
.flatMapMany { unspents -> .flatMapMany { unspents ->
val result = getTotal(chain, addresses, unspents) val result = getTotal(chain, addresses, unspents)
@@ -58,7 +58,7 @@ class TrackBitcoinAddress(
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
@@ -107,7 +107,7 @@ class TrackBitcoinAddress(
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
if (addresses.isEmpty()) { if (addresses.isEmpty()) {

View File

@@ -6,9 +6,9 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -37,7 +37,7 @@ class TrackBitcoinTx(
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> { override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
val chain = Chain.byId(request.chainValue) val chain = Chain.byId(request.chainValue)
val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) val upstream = upstreams.getUpstream(chain)?.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(chain)) ?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
val txid = request.txId val txid = request.txId
val confirmations = max(min(1, request.confirmationLimit), 12) val confirmations = max(min(1, request.confirmationLimit), 12)
@@ -48,7 +48,7 @@ class TrackBitcoinTx(
}.map(this::asProto) }.map(this::asProto)
} }
fun subscribe(chain: Chain, api: BitcoinApi, upstream: Upstream<BitcoinApi>, txid: String): Flux<TxStatus> { fun subscribe(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> {
return loadExisting(api, txid) return loadExisting(api, txid)
.flatMapMany { status -> .flatMapMany { status ->
if (status.mined) { if (status.mined) {
@@ -56,7 +56,7 @@ class TrackBitcoinTx(
//without publishing an empty TxStatus first //without publishing an empty TxStatus first
continueWithMined(api, upstream, status) continueWithMined(api, upstream, status)
} else { } else {
loadMempool(api, txid) loadMempool(upstream, txid)
.flatMapMany { tx -> .flatMapMany { tx ->
val next = if (tx.found) { val next = if (tx.found) {
untilMined(upstream, tx) untilMined(upstream, tx)
@@ -70,7 +70,7 @@ class TrackBitcoinTx(
} }
} }
fun continueWithMined(api: BitcoinApi, upstream: Upstream<BitcoinApi>, status: TxStatus): Flux<TxStatus> { fun continueWithMined(api: DirectBitcoinApi, upstream: BitcoinUpstream, status: TxStatus): Flux<TxStatus> {
return api.getBlock(status.blockHash!!) return api.getBlock(status.blockHash!!)
.map { block -> .map { block ->
TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block)) TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block))
@@ -79,10 +79,10 @@ class TrackBitcoinTx(
} }
} }
fun untilFound(chain: Chain, api: BitcoinApi, upstream: Upstream<BitcoinApi>, txid: String): Flux<TxStatus> { fun untilFound(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> {
return Flux.interval(Duration.ofSeconds(1)) return Flux.interval(Duration.ofSeconds(1))
.take(Duration.ofMinutes(10)) .take(Duration.ofMinutes(10))
.flatMap { loadMempool(api, txid) } .flatMap { loadMempool(upstream, txid) }
.skipUntil { it.found } .skipUntil { it.found }
.flatMap { subscribe(chain, api, upstream, txid) } .flatMap { subscribe(chain, api, upstream, txid) }
.doOnError { t -> .doOnError { t ->
@@ -90,7 +90,7 @@ class TrackBitcoinTx(
} }
} }
fun untilMined(upstream: Upstream<BitcoinApi>, tx: TxStatus): Mono<TxStatus> { fun untilMined(upstream: BitcoinUpstream, tx: TxStatus): Mono<TxStatus> {
return upstream.getHead().getFlux().flatMap { return upstream.getHead().getFlux().flatMap {
upstream.getApi(Selector.empty).flatMap { api -> upstream.getApi(Selector.empty).flatMap { api ->
loadExisting(api, tx.txid) loadExisting(api, tx.txid)
@@ -98,13 +98,13 @@ class TrackBitcoinTx(
}.single() }.single()
} }
fun withConfirmations(upstream: Upstream<BitcoinApi>, tx: TxStatus): Flux<TxStatus> { fun withConfirmations(upstream: BitcoinUpstream, tx: TxStatus): Flux<TxStatus> {
return upstream.getHead().getFlux().map { return upstream.getHead().getFlux().map {
tx.withHead(it.height) tx.withHead(it.height)
} }
} }
fun loadExisting(api: BitcoinApi, txid: String): Mono<TxStatus> { fun loadExisting(api: DirectBitcoinApi, txid: String): Mono<TxStatus> {
val mined = api.getTx(txid) val mined = api.getTx(txid)
return mined.map { return mined.map {
val block = it["blockhash"] as String? val block = it["blockhash"] as String?
@@ -112,8 +112,9 @@ class TrackBitcoinTx(
} }
} }
fun loadMempool(api: BitcoinApi, txid: String): Mono<TxStatus> { fun loadMempool(upstream: BitcoinUpstream, txid: String): Mono<TxStatus> {
val mempool = api.getMempool() println("access: ${upstream.getData()}")
val mempool = upstream.getData().getMempool().get()
return mempool.map { return mempool.map {
if (it.contains(txid)) { if (it.contains(txid)) {
TxStatus(txid, found = true, mined = false) TxStatus(txid, found = true, mined = false)

View File

@@ -20,10 +20,9 @@ import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.CurrentUpstreams import io.emeraldpay.dshackle.upstream.CurrentUpstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
@@ -134,11 +133,11 @@ open class ConfiguredUpstreams(
options: UpstreamsConfig.Options) { options: UpstreamsConfig.Options) {
val conn = config.connection!! val conn = config.connection!!
var rpcApi: BitcoinApi? = null var rpcApi: DirectBitcoinApi? = null
val methods = buildMethods(config, chain) val methods = buildMethods(config, chain)
conn.rpc?.let { endpoint -> conn.rpc?.let { endpoint ->
val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!)
rpcApi = BitcoinApi(rpcClient, objectMapper, methods) rpcApi = DirectBitcoinApi(rpcClient, objectMapper, methods)
} }
rpcApi?.let { api -> rpcApi?.let { api ->
val upstream = BitcoinUpstream(config.id val upstream = BitcoinUpstream(config.id

View File

@@ -20,7 +20,7 @@ import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.startup.UpstreamChange import io.emeraldpay.dshackle.startup.UpstreamChange
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods
@@ -70,10 +70,10 @@ class CurrentUpstreams(
} }
BlockchainType.BITCOIN -> { BlockchainType.BITCOIN -> {
val up = change.upstream val up = change.upstream
.cast(BitcoinUpstream::class.java, BitcoinApi::class.java) .cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
val current = chainMapping[chain] as ChainUpstreams<BitcoinApi>? val current = chainMapping[chain] as ChainUpstreams<DirectBitcoinApi>?
val factory = Callable { val factory = Callable {
BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<BitcoinApi> BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<DirectBitcoinApi>
} }
processUpdate(change, up, current, factory) processUpdate(change, up, current, factory)
} }

View File

@@ -14,7 +14,7 @@ class BitcoinChainUpstreams(
val upstreams: MutableList<BitcoinUpstream>, val upstreams: MutableList<BitcoinUpstream>,
caches: Caches, caches: Caches,
objectMapper: ObjectMapper objectMapper: ObjectMapper
) : ChainUpstreams<BitcoinApi>(chain, upstreams as MutableList<Upstream<BitcoinApi>>, caches, objectMapper) { ) : ChainUpstreams<DirectBitcoinApi>(chain, upstreams as MutableList<Upstream<DirectBitcoinApi>>, caches, objectMapper) {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java) private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java)
@@ -66,7 +66,7 @@ class BitcoinChainUpstreams(
} }
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> { override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(BitcoinApi::class.java)) { if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
} }
return this as Upstream<A> return this as Upstream<A>

View File

@@ -0,0 +1,33 @@
package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.upstream.Head
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
open class BitcoinData(
api: DirectBitcoinApi,
head: Head
) : Lifecycle {
companion object {
private val log = LoggerFactory.getLogger(BitcoinData::class.java)
}
private val mempool = CachingMempoolData(api, head)
open fun getMempool(): CachingMempoolData {
return mempool
}
override fun isRunning(): Boolean {
return mempool.isRunning
}
override fun start() {
mempool.start()
}
override fun stop() {
mempool.stop()
}
}

View File

@@ -3,7 +3,6 @@ package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.upstream.AbstractHead import io.emeraldpay.dshackle.upstream.AbstractHead
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.ethereum.EthereumRpcHead
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import org.springframework.scheduling.concurrent.CustomizableThreadFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory
@@ -15,7 +14,7 @@ import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
class BitcoinRpcHead( class BitcoinRpcHead(
private val api: BitcoinApi, private val api: DirectBitcoinApi,
private val extractBlock: ExtractBlock, private val extractBlock: ExtractBlock,
private val interval: Duration = Duration.ofSeconds(15) private val interval: Duration = Duration.ofSeconds(15)
) : Head, AbstractHead(), Lifecycle { ) : Head, AbstractHead(), Lifecycle {

View File

@@ -6,21 +6,20 @@ import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.JacksonRpcConverter
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle import org.springframework.context.Lifecycle
import reactor.core.Disposable import reactor.core.Disposable
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
class BitcoinUpstream( open class BitcoinUpstream(
id: String, id: String,
val chain: Chain, val chain: Chain,
private val api: BitcoinApi, private val api: DirectBitcoinApi,
options: UpstreamsConfig.Options, options: UpstreamsConfig.Options,
val node: QuorumForLabels.QuorumItem, val node: QuorumForLabels.QuorumItem,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
callMethods: CallMethods callMethods: CallMethods
) : DefaultUpstream<BitcoinApi>(id, options, callMethods), Lifecycle { ) : DefaultUpstream<DirectBitcoinApi>(id, options, callMethods), Lifecycle {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
@@ -28,6 +27,7 @@ class BitcoinUpstream(
private val head: Head = createHead() private val head: Head = createHead()
private var validatorSubscription: Disposable? = null private var validatorSubscription: Disposable? = null
private val data = BitcoinData(api, head)
private fun createHead(): Head { private fun createHead(): Head {
return BitcoinRpcHead( return BitcoinRpcHead(
@@ -36,11 +36,15 @@ class BitcoinUpstream(
) )
} }
open fun getData(): BitcoinData {
return data
}
override fun getHead(): Head { override fun getHead(): Head {
return head return head
} }
override fun getApi(matcher: Selector.Matcher): Mono<out BitcoinApi> { override fun getApi(matcher: Selector.Matcher): Mono<out DirectBitcoinApi> {
return Mono.just(api) return Mono.just(api)
} }
@@ -56,8 +60,8 @@ class BitcoinUpstream(
} }
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> { override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(BitcoinApi::class.java)) { if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
throw ClassCastException("Cannot cast ${BitcoinApi::class.java} to $apiType") throw ClassCastException("Cannot cast ${DirectBitcoinApi::class.java} to $apiType")
} }
return this as Upstream<A> return this as Upstream<A>
} }
@@ -67,6 +71,7 @@ class BitcoinUpstream(
if (head is Lifecycle) { if (head is Lifecycle) {
runningAny = runningAny || head.isRunning runningAny = runningAny || head.isRunning
} }
runningAny = runningAny || data.isRunning
return runningAny return runningAny
} }
@@ -77,6 +82,8 @@ class BitcoinUpstream(
head.start() head.start()
} }
} }
data.start()
validatorSubscription?.dispose() validatorSubscription?.dispose()
if (getOptions().disableValidation != null && getOptions().disableValidation!!) { if (getOptions().disableValidation != null && getOptions().disableValidation!!) {
@@ -93,6 +100,7 @@ class BitcoinUpstream(
if (head is Lifecycle) { if (head is Lifecycle) {
head.stop() head.stop()
} }
data.stop()
validatorSubscription?.dispose() validatorSubscription?.dispose()
} }

View File

@@ -1,10 +1,7 @@
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.infinitape.etherjar.rpc.Commands
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.scheduling.concurrent.CustomizableThreadFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -14,7 +11,7 @@ import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
class BitcoinUpstreamValidator( class BitcoinUpstreamValidator(
private val api: BitcoinApi, private val api: DirectBitcoinApi,
private val options: UpstreamsConfig.Options private val options: UpstreamsConfig.Options
) { ) {

View File

@@ -0,0 +1,71 @@
package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.upstream.Head
import org.slf4j.LoggerFactory
import org.springframework.context.Lifecycle
import reactor.core.Disposable
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
open class CachingMempoolData(
private val api: DirectBitcoinApi,
private val head: Head
) : Lifecycle {
companion object {
private val log = LoggerFactory.getLogger(CachingMempoolData::class.java)
private val TTL = Duration.ofSeconds(15)
}
private val current = AtomicReference<Container>(Container.empty())
private val updateLock = ReentrantLock()
private var headListener: Disposable? = null
open fun get(): Mono<List<String>> {
val value = current.get()
return if (value.since < Instant.now().minus(TTL)) {
updateLock.lock()
fetchFromUpstream()
.timeout(Duration.ofSeconds(3), Mono.empty())
.doOnNext {
current.set(Container(Instant.now(), it))
}.doFinally {
updateLock.unlock()
}
} else {
Mono.just(value.value)
}
}
fun fetchFromUpstream(): Mono<List<String>> {
return api.executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono<List<String>>
}
class Container(val since: Instant, val value: List<String>) {
companion object {
fun empty(): Container {
return Container(Instant.MIN, emptyList())
}
}
}
override fun isRunning(): Boolean {
return headListener != null
}
override fun start() {
headListener?.dispose()
headListener = head.getFlux().doOnNext {
current.set(Container.empty())
}.subscribe()
}
override fun stop() {
headListener?.dispose()
headListener = null
}
}

View File

@@ -14,14 +14,14 @@ import io.infinitape.etherjar.rpc.json.ResponseJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
open class BitcoinApi( open class DirectBitcoinApi(
val bitcoinRpcClient: BitcoinRpcClient, val bitcoinRpcClient: BitcoinRpcClient,
val objectMapper: ObjectMapper, val objectMapper: ObjectMapper,
val targets: CallMethods val targets: CallMethods
) : UpstreamApi { ) : UpstreamApi {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinApi::class.java) private val log = LoggerFactory.getLogger(DirectBitcoinApi::class.java)
} }
open override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> { open override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
@@ -104,7 +104,4 @@ open class BitcoinApi(
return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono<Map<String, Any>> return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono<Map<String, Any>>
} }
open fun getMempool(): Mono<List<String>> {
return executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono<List<String>>
}
} }

View File

@@ -9,7 +9,7 @@ import io.emeraldpay.dshackle.upstream.AggregatedUpstream
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -201,7 +201,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Get update for a balance"() { def "Get update for a balance"() {
setup: setup:
BitcoinApi api = Mock(BitcoinApi) { DirectBitcoinApi api = Mock(DirectBitcoinApi) {
2 * executeAndResult(0, "listunspent", [], List) >>> [ 2 * executeAndResult(0, "listunspent", [], List) >>> [
Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]]) Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
] ]

View File

@@ -5,7 +5,10 @@ import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinData
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 io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -20,14 +23,20 @@ class TrackBitcoinTxSpec extends Specification {
def "loadMempool() returns not found when not found"() { def "loadMempool() returns not found when not found"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
BitcoinApi api = Mock(BitcoinApi) {
1 * getMempool() >> Mono.just([ CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
1 * get() >> Mono.just([
"69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9",
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
]) ])
} }
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
_ * getData() >> Mock(BitcoinData) {
_ * getMempool() >> mempoolAccess
}
}
when: when:
def act = track.loadMempool(api, "65ce58db064bd105b14dc76a0bce0df14653cf5263d22d17e78864cf272ee367") def act = track.loadMempool(upstream, "65ce58db064bd105b14dc76a0bce0df14653cf5263d22d17e78864cf272ee367")
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -41,14 +50,19 @@ class TrackBitcoinTxSpec extends Specification {
def "loadMempool() returns ok when found"() { def "loadMempool() returns ok when found"() {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
BitcoinApi api = Mock(BitcoinApi) { CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
1 * getMempool() >> Mono.just([ 1 * get() >> Mono.just([
"69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9",
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
]) ])
} }
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
_ * getData() >> Mock(BitcoinData) {
_ * getMempool() >> mempoolAccess
}
}
when: when:
def act = track.loadMempool(api, "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9") def act = track.loadMempool(upstream, "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9")
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -63,7 +77,7 @@ class TrackBitcoinTxSpec extends Specification {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
BitcoinApi api = Mock(BitcoinApi) { DirectBitcoinApi api = Mock(DirectBitcoinApi) {
1 * getTx(txid) >> Mono.just([ 1 * getTx(txid) >> Mono.just([
txid: txid txid: txid
]) ])
@@ -84,7 +98,7 @@ class TrackBitcoinTxSpec extends Specification {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
BitcoinApi api = Mock(BitcoinApi) { DirectBitcoinApi api = Mock(DirectBitcoinApi) {
1 * getTx(txid) >> Mono.just([ 1 * getTx(txid) >> Mono.just([
txid : txid, txid : txid,
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
@@ -116,7 +130,7 @@ class TrackBitcoinTxSpec extends Specification {
Head head = Mock(Head) { Head head = Mock(Head) {
1 * getFlux() >> next 1 * getFlux() >> next
} }
Upstream upstream = Mock(Upstream) { Upstream upstream = Mock(BitcoinUpstream) {
1 * getHead() >> head 1 * getHead() >> head
} }
def status = new TrackBitcoinTx.TxStatus( def status = new TrackBitcoinTx.TxStatus(
@@ -147,7 +161,7 @@ class TrackBitcoinTxSpec extends Specification {
Head head = Mock(Head) { Head head = Mock(Head) {
1 * getFlux() >> next 1 * getFlux() >> next
} }
BitcoinApi api = Mock(BitcoinApi) { DirectBitcoinApi api = Mock(DirectBitcoinApi) {
3 * getTx(txid) >>> [ 3 * getTx(txid) >>> [
Mono.just([ Mono.just([
txid: txid txid: txid
@@ -162,7 +176,7 @@ class TrackBitcoinTxSpec extends Specification {
]) ])
] ]
} }
Upstream upstream = Mock(Upstream) { Upstream upstream = Mock(BitcoinUpstream) {
1 * getHead() >> head 1 * getHead() >> head
_ * getApi(_) >> Mono.just(api) _ * getApi(_) >> Mono.just(api)
} }
@@ -183,13 +197,7 @@ class TrackBitcoinTxSpec extends Specification {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
BitcoinApi api = Mock(BitcoinApi) { DirectBitcoinApi api = Mock(DirectBitcoinApi) {
4 * getMempool() >>> [
Mono.just([]),
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9"]),
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]),
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over
]
1 * getTx(txid) >> Mono.just([ 1 * getTx(txid) >> Mono.just([
txid: txid txid: txid
]) ])
@@ -197,9 +205,20 @@ class TrackBitcoinTxSpec extends Specification {
Head head = Mock(Head) { Head head = Mock(Head) {
_ * getFlux() >> Flux.empty() _ * getFlux() >> Flux.empty()
} }
Upstream upstream = Mock(Upstream) { CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
4 * get() >>> [
Mono.just([]),
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9"]),
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]),
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over
]
}
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
_ * getApi(_) >> Mono.just(api) _ * getApi(_) >> Mono.just(api)
_ * getHead() >> head _ * getHead() >> head
_ * getData() >> Mock(BitcoinData) {
_ * getMempool() >> mempoolAccess
}
} }
when: when:
@@ -220,7 +239,7 @@ class TrackBitcoinTxSpec extends Specification {
setup: setup:
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
BitcoinApi api = Mock(BitcoinApi) { DirectBitcoinApi api = Mock(DirectBitcoinApi) {
_ * getTx(txid) >> Mono.just([ _ * getTx(txid) >> Mono.just([
txid : txid, txid : txid,
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
@@ -238,7 +257,7 @@ class TrackBitcoinTxSpec extends Specification {
Head head = Mock(Head) { Head head = Mock(Head) {
_ * getFlux() >> next _ * getFlux() >> next
} }
Upstream upstream = Mock(Upstream) { BitcoinUpstream upstream = Mock(BitcoinUpstream) {
_ * getApi(_) >> Mono.just(api) _ * getApi(_) >> Mono.just(api)
_ * getHead() >> head _ * getHead() >> head
} }

View File

@@ -59,7 +59,7 @@ class BitcoinRpcHeadSpec extends Specification {
} }
""" """
BitcoinApi api = Mock(BitcoinApi) { DirectBitcoinApi api = Mock(DirectBitcoinApi) {
_ * executeAndResult(_, "getbestblockhash", _, String) >>> [ _ * executeAndResult(_, "getbestblockhash", _, String) >>> [
Mono.just(hash1), Mono.just(hash1), Mono.just(hash2) Mono.just(hash1), Mono.just(hash1), Mono.just(hash2)
] ]

View File

@@ -10,14 +10,14 @@ import spock.lang.Specification
import java.time.Duration import java.time.Duration
class BitcoinApiSpec extends Specification { class DirectBitcoinApiSpec extends Specification {
ClientAndServer mockServer ClientAndServer mockServer
BitcoinApi api DirectBitcoinApi api
def setup() { def setup() {
mockServer = ClientAndServer.startClientAndServer(18332); mockServer = ClientAndServer.startClientAndServer(18332);
api = new BitcoinApi( api = new DirectBitcoinApi(
new BitcoinRpcClient("localhost:18332", null), new BitcoinRpcClient("localhost:18332", null),
TestingCommons.objectMapper(), new DefaultBitcoinMethods(TestingCommons.objectMapper()) TestingCommons.objectMapper(), new DefaultBitcoinMethods(TestingCommons.objectMapper())
) )