Logs subscription via eth_getLogs (#356)

move retrieving logs subscription to eth_getLogs by hash
This commit is contained in:
Vyacheslav
2023-12-04 17:09:32 +02:00
committed by GitHub
parent 9696f06732
commit 0a47da602a
14 changed files with 157 additions and 769 deletions

View File

@@ -493,8 +493,6 @@ abstract class Multistream(
abstract fun getHead(mather: Selector.Matcher): Head
abstract fun getEnrichedHead(mather: Selector.Matcher): Head
// --------------------------------------------------------------------------------------------------------
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability)

View File

@@ -125,10 +125,6 @@ open class BitcoinMultistream(
return head
}
override fun getEnrichedHead(mather: Matcher): Head {
TODO("Not yet implemented")
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return sourceUpstreams.flatMap { it.getLabels() }
}

View File

@@ -159,7 +159,7 @@ class EthereumCallSelector(
}
}
private fun blockByHash(blockHash: String): Mono<Selector.Matcher> {
fun blockByHash(blockHash: String): Mono<Selector.Matcher> {
return try {
caches.getLastHeightByHash()
.read(BlockId.from(blockHash))

View File

@@ -1,100 +0,0 @@
package io.emeraldpay.dshackle.upstream.ethereum
import com.google.common.cache.CacheBuilder
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.etherjar.domain.BlockHash
import reactor.core.Disposable
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks
import reactor.core.scheduler.Scheduler
import java.time.Duration
class EnrichedMergedHead constructor(
private val sources: Iterable<Head>,
private val referenceHead: Head,
private val headScheduler: Scheduler,
private val api: Reader<BlockHash, BlockContainer>,
) : Head, Lifecycle {
private val enrichedBlocks = CacheBuilder.newBuilder()
.maximumSize(10)
.build<BlockId, BlockContainer>()
private val enrichedPromises = CacheBuilder.newBuilder()
.maximumSize(10)
.build<BlockId, Sinks.One<BlockContainer>>()
private var cacheSub: Disposable? = null
private fun getEnrichBlockMono(id: BlockId): Mono<BlockContainer> {
val block = enrichedBlocks.getIfPresent(id)
return if (block != null) {
Mono.just(block)
} else {
enrichedPromises.get(id) {
Sinks.one()
}.asMono()
}
}
override fun getFlux(): Flux<BlockContainer> {
return referenceHead.getFlux().concatMap { block ->
if (block.enriched) {
Mono.just(block)
} else {
Mono.firstWithValue(
getEnrichBlockMono(block.hash),
Mono.just(block)
.delayElement(Duration.ofSeconds(1))
.flatMap {
EthereumBlockEnricher.enrich(BlockHash(block.hash.value), api, headScheduler)
},
)
}
}
}
override fun onBeforeBlock(handler: Runnable) {}
override fun getCurrentHeight(): Long? {
return referenceHead.getCurrentHeight()
}
override fun getCurrentSlotHeight(): Long? {
return referenceHead.getCurrentSlotHeight()
}
override fun isRunning(): Boolean {
return cacheSub != null
}
override fun start() {
cacheSub?.dispose()
sources.forEach { head ->
if (head is Lifecycle && !head.isRunning()) {
head.start()
}
}
if (referenceHead is Lifecycle && !referenceHead.isRunning()) {
referenceHead.start()
}
cacheSub = Flux.merge(sources.map { it.getFlux() }).subscribe { block ->
if (block.enriched) {
enrichedBlocks.put(block.hash, block)
enrichedPromises.get(block.hash) { Sinks.one() }.tryEmitValue(block)
}
}
}
override fun stop() {
cacheSub?.dispose()
cacheSub = null
}
override fun onSyncingNode(isSyncing: Boolean) {}
override fun headLiveness(): Flux<Boolean> = Flux.empty()
}

View File

@@ -1,29 +0,0 @@
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.etherjar.domain.BlockHash
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import reactor.retry.Repeat
import java.time.Duration
class EthereumBlockEnricher {
companion object {
fun enrich(blockHash: BlockHash, api: Reader<BlockHash, BlockContainer>, scheduler: Scheduler): Mono<BlockContainer> {
return Mono.just(blockHash)
.flatMap { hash ->
api.read(hash)
.subscribeOn(scheduler)
.timeout(Defaults.timeoutInternal, Mono.empty())
}.repeatWhenEmpty { n ->
Repeat.times<Any>(5)
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
.apply(n)
}
.timeout(Defaults.timeout, Mono.empty())
.onErrorResume { Mono.empty() }
}
}
}

View File

@@ -43,6 +43,7 @@ import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.TransactionLogJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.apache.commons.collections4.Factory
import org.springframework.cloud.sleuth.Tracer
@@ -133,6 +134,10 @@ open class EthereumCachingReader(
)
}
open fun logsByHash(): Reader<BlockId, Result<List<TransactionLogJson>>> {
return directReader.logsByHashReader
}
open fun txByHashAsCont(): Reader<TxId, Result<TxContainer>> {
return CompoundReader(
CacheWithUpstreamIdReader(SpannedReader(caches.getTxByHash(), tracer, CACHE_TX_BY_HASH_READER)),

View File

@@ -16,6 +16,7 @@ import io.emeraldpay.dshackle.reader.RpcReaderFactory
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.EthereumCallSelector
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -26,6 +27,7 @@ import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.hex.HexQuantity
import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.emeraldpay.etherjar.rpc.json.TransactionLogJson
import io.emeraldpay.etherjar.rpc.json.TransactionReceiptJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.apache.commons.collections4.Factory
@@ -59,6 +61,7 @@ class EthereumDirectReader(
val txReader: Reader<TransactionId, Result<TxContainer>>
val balanceReader: Reader<Address, Result<Wei>>
val receiptReader: Reader<TransactionId, Result<ByteArray>>
val logsByHashReader: Reader<BlockId, Result<List<TransactionLogJson>>>
init {
blockReader = object : Reader<BlockHash, Result<BlockContainer>> {
@@ -120,6 +123,7 @@ class EthereumDirectReader(
}
}
}
receiptReader = object : Reader<TransactionId, Result<ByteArray>> {
override fun read(key: TransactionId): Mono<Result<ByteArray>> {
val request = JsonRpcRequest("eth_getTransactionReceipt", listOf(key.toHex()))
@@ -148,6 +152,33 @@ class EthereumDirectReader(
}
}
}
logsByHashReader = object : Reader<BlockId, Result<List<TransactionLogJson>>> {
override fun read(key: BlockId): Mono<Result<List<TransactionLogJson>>> {
val request = JsonRpcRequest(
"eth_getLogs",
listOf(
mapOf(
"blockHash" to key.toHexWithPrefix(),
),
),
)
return EthereumCallSelector(caches).blockByHash(key.toHexWithPrefix())
.defaultIfEmpty(Selector.empty).flatMap { matcher ->
readWithQuorum(request, matcher)
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Logs not read $key")))
.flatMap {
val logs = objectMapper.readValue(it.data, Array<TransactionLogJson>::class.java)?.toList()
if (logs == null) {
log.debug("Empty logs for block $key")
Mono.empty()
} else {
Mono.just(Result(logs, it.upstreamId))
}
}
}
}
}
}
@Suppress("UNCHECKED_CAST")

View File

@@ -17,7 +17,6 @@ package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
@@ -51,7 +50,7 @@ class ConnectBlockUpdates(
fun connect() = connect(Selector.empty)
override fun connect(matcher: Selector.Matcher): Flux<Update> {
return connected.computeIfAbsent(matcher.describeInternal()) { key ->
extract(upstream.getEnrichedHead(matcher))
extract(upstream.getHead(matcher))
.publishOn(scheduler)
.publish()
.refCount(1, Duration.ofSeconds(60))
@@ -104,35 +103,31 @@ class ConnectBlockUpdates(
* Produce updates for transactions when a block is replaces with a different one on the same height.
*/
fun whenReplaced(prev: BlockContainer, source: String): Flux<Update> {
return Flux.fromIterable(prev.transactions).map {
return Flux.just(
Update(
prev.hash,
prev.height,
UpdateType.DROP,
it,
source,
)
}
),
)
}
fun extractUpdates(block: BlockContainer): Flux<Update> {
return Flux.fromIterable(block.transactions)
.map {
Update(
block.hash,
block.height,
UpdateType.NEW,
it,
block.upstreamId,
)
}
return Flux.just(
Update(
block.hash,
block.height,
UpdateType.NEW,
block.upstreamId,
),
)
}
data class Update(
val blockHash: BlockId,
val blockNumber: Long,
val type: UpdateType,
val transactionId: TxId,
val upstreamId: String,
)

View File

@@ -17,16 +17,14 @@ package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import com.google.common.cache.CacheBuilder
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader.Result
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
import io.emeraldpay.etherjar.hex.HexData
import io.emeraldpay.etherjar.rpc.json.TransactionReceiptJson
import io.emeraldpay.etherjar.rpc.json.TransactionLogJson
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@@ -34,7 +32,7 @@ import reactor.kotlin.core.publisher.switchIfEmpty
import java.util.concurrent.TimeUnit
class ProduceLogs(
private val receipts: Reader<TxId, Result<ByteArray>>,
private val logs: Reader<BlockId, Result<List<TransactionLogJson>>>,
private val chain: Chain,
) {
@@ -43,17 +41,13 @@ class ProduceLogs(
}
constructor(upstream: Multistream) :
this((upstream.getCachingReader() as EthereumCachingReader).receipts(), (upstream as Multistream).chain)
private val objectMapper = Global.objectMapper
this((upstream.getCachingReader() as EthereumCachingReader).logsByHash(), (upstream as Multistream).chain)
// need to keep history of recent messages in case they get removed. cannot rely on
// any other cache or upstream because if when it gets removed it's unavailable in any other source
private val oldMessages = CacheBuilder.newBuilder()
// in general a block with its events can be replaced in ~90 seconds, but in case of a big network disturbance
// it can be much longer. here we keep events up to an hour
.expireAfterWrite(60, TimeUnit.MINUTES)
.maximumSize(90000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(30)
.build<LogReference, List<LogMessage>>()
fun produce(block: Flux<ConnectBlockUpdates.Update>): Flux<LogMessage> {
@@ -67,10 +61,10 @@ class ProduceLogs(
}
fun produceRemoved(update: ConnectBlockUpdates.Update): Flux<LogMessage> {
val old = oldMessages.getIfPresent(LogReference(update.blockHash, update.transactionId))
val old = oldMessages.getIfPresent(LogReference(update.blockHash))
if (old == null) {
log.warn(
"No old message to produce removal messages for tx ${update.transactionId} " +
"No old message to produce removal messages " +
"at block ${update.blockHash} for chain ${chain.chainName}",
)
return Flux.empty()
@@ -80,48 +74,33 @@ class ProduceLogs(
}
fun produceAdded(update: ConnectBlockUpdates.Update): Flux<LogMessage> {
return receipts.read(update.transactionId)
.switchIfEmpty {
log.warn("Cannot find receipt for tx ${update.transactionId} for chain ${chain.chainName}")
Mono.empty()
}
.map { it.data }
.flatMapMany { jsonBytes ->
// receipt could be a null, like when the original block was replaced, etc.
// so just skip it as Flux.empty
val receipt = objectMapper.readValue(jsonBytes, TransactionReceiptJson::class.java)
?: return@flatMapMany Flux.empty<LogMessage>()
try {
val messages = receipt.logs
.map { txlog ->
LogMessage(
txlog.address,
txlog.blockHash,
txlog.blockNumber,
txlog.data ?: HexData.empty(),
txlog.logIndex,
txlog.topics,
txlog.transactionHash,
txlog.transactionIndex,
false,
update.upstreamId,
return logs.read(update.blockHash).switchIfEmpty {
log.warn("Cannot find receipt for block ${update.blockHash} for chain ${chain.chainName}")
Mono.empty()
}.map {
it.data
}.flatMapMany {
val messages = it.map { log ->
LogMessage(
log.address,
log.blockHash,
log.blockNumber,
log.data ?: HexData.empty(),
log.logIndex,
log.topics,
log.transactionHash,
log.transactionIndex,
false,
update.upstreamId,
)
}
oldMessages.put(LogReference(update.blockHash, update.transactionId), messages)
Flux.fromIterable(messages)
} catch (t: Throwable) {
log.warn(
"Invalid Receipt ${update.transactionId} for chain ${chain.chainName}. " +
"${t.javaClass}: ${t.message}",
)
Flux.empty<LogMessage>()
}
)
}
oldMessages.put(LogReference(update.blockHash), messages)
Flux.fromIterable(messages)
}
}
private data class LogReference(
val block: BlockId,
val tx: TxId,
)
}

View File

@@ -21,9 +21,7 @@ import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.IndexConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
@@ -39,11 +37,8 @@ import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Selector.Matcher
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.calls.CallSelector
import io.emeraldpay.dshackle.upstream.ethereum.EnrichedMergedHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
import io.emeraldpay.etherjar.domain.BlockHash
import org.springframework.util.ConcurrentReferenceHashMap
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.WEAK
import reactor.core.publisher.Flux
@@ -157,27 +152,6 @@ open class GenericMultistream(
return head
}
override fun getEnrichedHead(mather: Selector.Matcher): Head =
filteredHeads.computeIfAbsent(mather.describeInternal().intern()) { _ ->
upstreams.filter { mather.matches(it) }
.apply {
log.debug("Found $size upstreams matching [${mather.describeInternal()}]")
}.let {
val selected = it.map { source -> source.getHead() }
EnrichedMergedHead(
selected,
getHead(),
headScheduler,
object :
Reader<BlockHash, BlockContainer> {
override fun read(key: BlockHash): Mono<BlockContainer> {
return (cachingReader as EthereumCachingReader).blocksByHashAsCont().read(key).map { res -> res.data }
}
},
)
}
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreams.flatMap { it.getLabels() }
}

View File

@@ -1,185 +0,0 @@
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.test.ApiReaderMock
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.AbstractHead
import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.domain.BlockHash
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks
import reactor.core.scheduler.Schedulers
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
class EnrichedMergedHeadSpec extends Specification {
def "ensures that heads are running on start"() {
setup:
def head1 = Mock(TestHead) {
_ * isRunning() >> false
_ * getFlux() >> Flux.empty()
}
def head2 = Mock(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> Flux.empty()
}
def head3 = Mock(TestHead) {
_ * isRunning() >> false
_ * getFlux() >> Flux.empty()
}
def api = new ApiReaderMock()
when:
def merged = new EnrichedMergedHead([head1, head2], head3, Schedulers.boundedElastic(), new BlockReader(api))
merged.start()
then:
1 * head3.start()
1 * head1.start()
merged.isRunning()
}
def "if enriched block comes from reference head we pass it along instantly"() {
setup:
def block = TestingCommons.enrichedBlockForEthereum(100)
def api = new ApiReaderMock()
def head = Stub(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> Flux.just(block)
}
when:
def merge = new EnrichedMergedHead([], head, Schedulers.boundedElastic(), new BlockReader(api))
then:
StepVerifier.create(merge.getFlux())
.then { merge.start() }
.expectNext(block)
.thenCancel()
.verify(Duration.ofMillis(100))
block.enriched
}
def "enriched block arrived in sources before reference block"() {
setup:
def enrichedBlock = TestingCommons.enrichedBlockForEthereum(100)
def block = TestingCommons.blockForEthereum(100)
Sinks.Many<BlockContainer> refSink = Sinks.many().multicast().directBestEffort()
def headRef = Stub(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> refSink.asFlux()
}
def headSource = Stub(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> Flux.just(enrichedBlock)
}
when:
def merge = new EnrichedMergedHead([headSource], headRef, Schedulers.boundedElastic(), new BlockReader(new ApiReaderMock()))
then:
StepVerifier.create(merge.getFlux())
.then { merge.start() }
.expectNoEvent(Duration.ofMillis(100))
.then { refSink.tryEmitNext(block) }
.expectNext(enrichedBlock)
.thenCancel()
.verify(Duration.ofMillis(200))
}
def "enriched block arrived in source after reference block, but before deadline"() {
setup:
def enrichedBlock = TestingCommons.enrichedBlockForEthereum(100)
def block = TestingCommons.blockForEthereum(100)
Sinks.Many<BlockContainer> sourceSink = Sinks.many().multicast().directBestEffort()
def headRef = Stub(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> Flux.just(block)
}
def headSource = Stub(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> sourceSink.asFlux()
}
when:
def merge = new EnrichedMergedHead([headSource], headRef, Schedulers.boundedElastic(), new BlockReader(new ApiReaderMock()))
then:
StepVerifier.create(merge.getFlux())
.then { merge.start() }
.expectNoEvent(Duration.ofMillis(600))
.then { sourceSink.tryEmitNext(enrichedBlock) }
.expectNext(enrichedBlock)
.thenCancel()
.verify(Duration.ofSeconds(1))
}
def "enriched blocks does not arrive before deadline"() {
setup:
def enrichedBlock = TestingCommons.enrichedBlockForEthereum(100)
def block = TestingCommons.blockForEthereum(100)
def headRef = Stub(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> Flux.just(block)
}
def headSource = Stub(TestHead) {
_ * isRunning() >> true
_ * getFlux() >> Flux.just(block)
}
def api = new ApiReaderMock().tap {
answer("eth_getBlockByHash", [block.hash.toHexWithPrefix(), false], enrichedBlock.toBlock())
}
when:
def merge = new EnrichedMergedHead([headSource], headRef, Schedulers.boundedElastic(), new BlockReader(api))
then:
StepVerifier.create(merge.getFlux())
.then { merge.start() }
.expectNoEvent(Duration.ofSeconds(1))
.expectNext(enrichedBlock)
.thenCancel()
.verify(Duration.ofMillis(1200))
}
class BlockReader implements Reader<BlockHash, BlockContainer> {
private ApiReaderMock mockApi
BlockReader(ApiReaderMock api) {
mockApi = api
}
Mono<BlockContainer> read(BlockHash hash) {
return mockApi.read(new JsonRpcRequest("eth_getBlockByHash", [hash.toHex(), false]))
.map {
def t = it
def a = 1
return it
}
.flatMap(JsonRpcResponse::requireResult)
.map { BlockContainer.fromEthereumJson(it, "test") }
}
}
class TestHead extends AbstractHead implements Lifecycle {
TestHead() {
super(new MostWorkForkChoice(), Schedulers.boundedElastic(), new BlockValidator.AlwaysValid(), 100_000)
}
@Override
void start() {
}
@Override
void stop() {
}
@Override
boolean isRunning() {
return false
}
}
}

View File

@@ -4,6 +4,7 @@ import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CurrentBlockCache
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.DefaultContainer
import io.emeraldpay.dshackle.reader.RpcReader
import io.emeraldpay.dshackle.reader.RpcReaderFactory
@@ -17,6 +18,7 @@ import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.domain.Wei
import io.emeraldpay.etherjar.rpc.json.TransactionJson
import io.emeraldpay.etherjar.rpc.json.TransactionLogJson
import io.emeraldpay.etherjar.rpc.json.TransactionReceiptJson
import org.apache.commons.collections4.Factory
import reactor.core.publisher.Mono
@@ -128,6 +130,38 @@ class EthereumDirectReaderSpec extends Specification {
.verify(Duration.ofSeconds(1))
}
def "Reads logs by block hash"() {
setup:
def json = new TransactionLogJson().tap {
address = Address.from(address1)
blockHash = BlockHash.from(hash1)
}
def calls = Mock(Factory) {
1 * create() >> new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET, false)
}
EthereumDirectReader reader = new EthereumDirectReader(
Stub(Multistream), Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
)
reader.rpcReaderFactory = Mock(RpcReaderFactory) {
1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getLogs", [Map.of("blockHash", hash1)])) >> Mono.just(
new RpcReader.Result(
Global.objectMapper.writeValueAsBytes([json]), null, 1, resolver
)
)
}
}
when:
def act = reader.logsByHashReader.read(BlockId.from(hash1))
then:
StepVerifier.create(act)
.expectNextMatches { logs ->
logs.data[0].blockHash == BlockHash.from(hash1)
}
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Reads tx"() {
setup:
def json = new TransactionJson().tap {

View File

@@ -45,27 +45,17 @@ class ConnectBlockUpdatesSpec extends Specification {
totalDifficulty = BigInteger.ONE
timestamp = Instant.now()
parentHash = parent
transactions = [
new TransactionRefJson(TransactionId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")),
new TransactionRefJson(TransactionId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2"))
]
transactions = []
})
when:
def act = connectBlockUpdates.extractUpdates(block)
.collectList().block(Duration.ofSeconds(3))
then:
act.size() == 2
act.size() == 1
with(act[0]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
it.transactionId == TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")
it.type == ConnectBlockUpdates.UpdateType.NEW
}
with(act[1]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
it.transactionId == TxId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2")
it.type == ConnectBlockUpdates.UpdateType.NEW
}
}
@@ -79,27 +69,17 @@ class ConnectBlockUpdatesSpec extends Specification {
totalDifficulty = BigInteger.ONE
timestamp = Instant.now()
parentHash = parent
transactions = [
new TransactionRefJson(TransactionId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")),
new TransactionRefJson(TransactionId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2"))
]
transactions = []
})
when:
def act = connectBlockUpdates.whenReplaced(block, "ConnectBlockUpdatesSpec")
.collectList().block(Duration.ofSeconds(3))
then:
act.size() == 2
act.size() == 1
with(act[0]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
it.transactionId == TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")
it.type == ConnectBlockUpdates.UpdateType.DROP
}
with(act[1]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
it.transactionId == TxId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2")
it.type == ConnectBlockUpdates.UpdateType.DROP
}
}
@@ -113,10 +93,7 @@ class ConnectBlockUpdatesSpec extends Specification {
totalDifficulty = BigInteger.ONE
timestamp = Instant.now()
parentHash = parent
transactions = [
new TransactionRefJson(TransactionId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")),
new TransactionRefJson(TransactionId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2"))
]
transactions = []
})
def block2 = BlockContainer.from(new BlockJson<TransactionRefJson>().tap {
hash = BlockHash.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")
@@ -124,10 +101,7 @@ class ConnectBlockUpdatesSpec extends Specification {
totalDifficulty = BigInteger.ONE
timestamp = Instant.now()
parentHash = parent
transactions = [
new TransactionRefJson(TransactionId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")),
new TransactionRefJson(TransactionId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2"))
]
transactions = []
})
def block3 = BlockContainer.from(new BlockJson<TransactionRefJson>().tap {
hash = BlockHash.from("0xdb1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7")
@@ -135,10 +109,7 @@ class ConnectBlockUpdatesSpec extends Specification {
totalDifficulty = BigInteger.ONE
parentHash = parent
timestamp = Instant.now()
transactions = [
new TransactionRefJson(TransactionId.from("0x9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af6c88df9d65ccc")),
new TransactionRefJson(TransactionId.from("0xdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe25c241a64e7ce536f"))
]
transactions = []
})
when:
@@ -177,10 +148,7 @@ class ConnectBlockUpdatesSpec extends Specification {
totalDifficulty = BigInteger.ONE
timestamp = Instant.now()
parentHash = parent
transactions = [
new TransactionRefJson(TransactionId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")),
new TransactionRefJson(TransactionId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2"))
]
transactions = []
})
def block2 = BlockContainer.from(new BlockJson<TransactionRefJson>().tap {
hash = BlockHash.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")
@@ -188,10 +156,7 @@ class ConnectBlockUpdatesSpec extends Specification {
totalDifficulty = BigInteger.ONE
timestamp = Instant.now()
parentHash = parent
transactions = [
new TransactionRefJson(TransactionId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")),
new TransactionRefJson(TransactionId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2"))
]
transactions = []
})
when:
@@ -200,29 +165,15 @@ class ConnectBlockUpdatesSpec extends Specification {
.collectList().block(Duration.ofSeconds(1))
then:
act.size() == 4
act.size() == 2
with(act[0]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
it.transactionId == TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")
it.type == ConnectBlockUpdates.UpdateType.DROP
}
with(act[1]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0xe5be2159b2b7daf6b126babdcbaa349da668b92d6b8c7db1350fd527fec4885c")
it.transactionId == TxId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2")
it.type == ConnectBlockUpdates.UpdateType.DROP
}
with(act[2]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")
it.transactionId == TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")
it.type == ConnectBlockUpdates.UpdateType.NEW
}
with(act[3]) {
it.blockNumber == 13412871
it.blockHash == BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")
it.transactionId == TxId.from("0x5c241a64e7ce536fdb6b8912091151f18a23dd71cc76a48a4b7d453e339efbe2")
it.type == ConnectBlockUpdates.UpdateType.NEW
}
}
@@ -233,7 +184,7 @@ class ConnectBlockUpdatesSpec extends Specification {
1 * getFlux() >> Flux.never()
}
def up = Mock(GenericMultistream) {
1 * getEnrichedHead(Selector.empty) >> head
1 * getHead(Selector.empty) >> head
}
def connectBlockUpdates = new ConnectBlockUpdates(up, Schedulers.boundedElastic())

View File

@@ -20,6 +20,11 @@ import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.hex.HexData
import io.emeraldpay.etherjar.rpc.json.TransactionLogJson
import reactor.core.publisher.Mono
import spock.lang.Specification
@@ -27,28 +32,28 @@ import java.time.Duration
class ProduceLogsSpec extends Specification {
def logs = [new TransactionLogJson().tap {
blockHash = BlockHash.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")
address = Address.from("0xe0aadb0a012dbcdc529c4c743d3e0385a0b54d3d")
blockNumber = 1
logIndex = 1
data = HexData.empty()
transactionIndex = 1
topics = []
transactionHash = TransactionId.from("0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec")
}]
def "Produce added as nothing with no logs"() {
setup:
String receipt = '{\n' +
' "blockHash": "0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da",\n' +
' "blockNumber": "0x1",\n' +
' "from": "0x5e78dd1e81ecdf078e029117eca98eaa71f46bdb",\n' +
' "logs": [\n' +
' ],\n' +
' "transactionHash": "0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af",\n' +
' "transactionIndex": "0x0"\n' +
' }'
def receipts = Mock(Reader) {
1 * it.read(TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")) >>
Mono.just(new EthereumDirectReader.Result<>(receipt.getBytes(), null))
def logs = Mock(Reader) {
1 * it.read(BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")) >>
Mono.just(new EthereumDirectReader.Result<>([], null))
}
def producer = new ProduceLogs(receipts, Chain.ETHEREUM__MAINNET)
def producer = new ProduceLogs(logs, Chain.ETHEREUM__MAINNET)
def update = new ConnectBlockUpdates.Update(
BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da"),
13412871,
1,
ConnectBlockUpdates.UpdateType.NEW,
TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af"),
"upstream"
)
when:
@@ -59,67 +64,18 @@ class ProduceLogsSpec extends Specification {
act.size() == 0
}
def "Produce added as nothing with null receipt"() {
def "Produce added with logs"() {
setup:
String receipt = 'null'
def receipts = Mock(Reader) {
1 * it.read(TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")) >>
Mono.just(new EthereumDirectReader.Result<>(receipt.getBytes(), null))
def logs = Mock(Reader) {
1 * it.read(BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")) >>
Mono.just(new EthereumDirectReader.Result<>(logs, null))
}
def producer = new ProduceLogs(receipts, Chain.ETHEREUM__MAINNET)
def producer = new ProduceLogs(logs, Chain.ETHEREUM__MAINNET)
def update = new ConnectBlockUpdates.Update(
BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da"),
13412871,
1,
ConnectBlockUpdates.UpdateType.NEW,
TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af"),
"upstream"
)
when:
def act = producer.produceAdded(update)
.collectList().block(Duration.ofSeconds(1))
then:
act.size() == 0
}
def "Produce added with single log"() {
setup:
String receipt = '{\n' +
' "blockHash": "0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da",\n' +
' "blockNumber": "0x1",\n' +
' "from": "0x5e78dd1e81ecdf078e029117eca98eaa71f46bdb",\n' +
' "logs": [\n' +
' {\n' +
' "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",\n' +
' "0x0000000000000000000000005e78dd1e81ecdf078e029117eca98eaa71f46bdb",\n' +
' "0x00000000000000000000000099897cb0e667d354b920fc38a40a5100b2a01566"\n' +
' ],\n' +
' "data": "0x00000000000000000000000000000000000000000000000000000007505d91f0",\n' +
' "blockNumber": "0xc7f3b4",\n' +
' "transactionHash": "0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af",\n' +
' "transactionIndex": "0x0",\n' +
' "blockHash": "0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da",\n' +
' "logIndex": "0x0",\n' +
' "removed": false\n' +
' }' +
' ],\n' +
' "transactionHash": "0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af",\n' +
' "transactionIndex": "0x0"\n' +
' }'
def receipts = Mock(Reader) {
1 * it.read(TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")) >>
Mono.just(new EthereumDirectReader.Result<>(receipt.getBytes(), null))
}
def producer = new ProduceLogs(receipts, Chain.ETHEREUM__MAINNET)
def update = new ConnectBlockUpdates.Update(
BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da"),
13412871,
ConnectBlockUpdates.UpdateType.NEW,
TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af"),
"upstream"
)
when:
@@ -128,244 +84,27 @@ class ProduceLogsSpec extends Specification {
then:
act.size() == 1
with(act[0]) {
it.transactionHash.toHex() == "0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af"
}
}
def "Produce added with no data"() {
setup:
String receipt = '{\n' +
' "blockHash": "0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da",\n' +
' "blockNumber": "0x1",\n' +
' "from": "0x5e78dd1e81ecdf078e029117eca98eaa71f46bdb",\n' +
' "logs": [\n' +
' {\n' +
' "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"\n' +
' ],\n' +
' "blockNumber": "0xc7f3b4",\n' +
' "transactionHash": "0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af",\n' +
' "transactionIndex": "0x0",\n' +
' "blockHash": "0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da",\n' +
' "logIndex": "0x0",\n' +
' "removed": false\n' +
' }' +
' ],\n' +
' "transactionHash": "0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af",\n' +
' "transactionIndex": "0x0"\n' +
' }'
def receipts = Mock(Reader) {
1 * it.read(TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af")) >>
Mono.just(new EthereumDirectReader.Result<>(receipt.getBytes(), null))
}
def producer = new ProduceLogs(receipts, Chain.ETHEREUM__MAINNET)
def update = new ConnectBlockUpdates.Update(
BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da"),
13412871,
ConnectBlockUpdates.UpdateType.NEW,
TxId.from("0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af"),
"upstream"
)
when:
def act = producer.produceAdded(update)
.collectList().block(Duration.ofSeconds(1))
then:
act.size() == 1
with(act[0]) {
it.transactionHash.toHex() == "0x6c88df9d65ccc9351db65676c3581b29483e8dabb71c48ef7671c44b0d5568af"
// Geth actually renders it as null, so this check may be wrong
it.data != null && it.data.size == 0
}
}
def "Produce added with multiple logs"() {
setup:
String receipt = '{\n' +
' "blockHash": "0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da",\n' +
' "blockNumber": "0x1",\n' +
' "from": "0x5e78dd1e81ecdf078e029117eca98eaa71f46bdb",\n' +
' "logs": [\n' +
' {\n' +
' "address": "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x00000000000000000000000075b8c48bdb04d426aed57b36bb835ad2dc321c30"\n' +
' ],\n' +
' "data": "0x0000000000000000000000000000000000000000000000013e7ec767db370000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb4",\n' +
' "removed": false\n' +
' },\n' +
' {\n' +
' "address": "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x0000000000000000000000000000000000000000000000000000000000000000"\n' +
' ],\n' +
' "data": "0x00000000000000000000000000000000000000000000000023636b7d513f0000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb5",\n' +
' "removed": false\n' +
' },\n' +
' {\n' +
' "address": "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x0000000000000000000000001b46b72c5280f30fbe8a958b4f3c348fd0fd2e55"\n' +
' ],\n' +
' "data": "0x00000000000000000000000000000000000000000000011316d590258fba0000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb6",\n' +
' "removed": false\n' +
' },\n' +
' {\n' +
' "address": "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",\n' +
' "topics": [\n' +
' "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x0000000000000000000000001b46b72c5280f30fbe8a958b4f3c348fd0fd2e55"\n' +
' ],\n' +
' "data": "0x0000000000000000000000000000000000000000000014188a101e403a500000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb7",\n' +
' "removed": false\n' +
' },\n' +
' {\n' +
' "address": "0x1b46b72c5280f30fbe8a958b4f3c348fd0fd2e55",\n' +
' "topics": [\n' +
' "0x90890809c654f11d6e72a28fa60149770a0d11ec6c92319d6ceb2bb0a4ea1a15",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x0000000000000000000000000000000000000000000000000000000000000000"\n' +
' ],\n' +
' "data": "0x00000000000000000000000000000000000000000000011478b7c30abc300000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb8",\n' +
' "removed": false\n' +
' }' +
' ],\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57"\n' +
' }'
def receipts = Mock(Reader) {
1 * it.read(TxId.from("0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec")) >>
Mono.just(new EthereumDirectReader.Result<>(receipt.getBytes(), null))
}
def producer = new ProduceLogs(receipts, Chain.ETHEREUM__MAINNET)
def update = new ConnectBlockUpdates.Update(
BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da"),
13412871,
ConnectBlockUpdates.UpdateType.NEW,
TxId.from("0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec"),
"upstream"
)
when:
def act = producer.produceAdded(update)
.collectList().block(Duration.ofSeconds(1))
then:
act.size() == 5
act*.transactionHash.every { it.toHex() == "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec" }
act*.logIndex == [180, 181, 182, 183, 184]
act*.address.every { it.toHex() == "0xe0aadb0a012dbcdc529c4c743d3e0385a0b54d3d" }
act*.removed.every { !it }
}
def "Produce removed"() {
setup:
String receipt = '{\n' +
' "blockHash": "0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da",\n' +
' "blockNumber": "0x1",\n' +
' "from": "0x5e78dd1e81ecdf078e029117eca98eaa71f46bdb",\n' +
' "logs": [\n' +
' {\n' +
' "address": "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x00000000000000000000000075b8c48bdb04d426aed57b36bb835ad2dc321c30"\n' +
' ],\n' +
' "data": "0x0000000000000000000000000000000000000000000000013e7ec767db370000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb4",\n' +
' "removed": false\n' +
' },\n' +
' {\n' +
' "address": "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x0000000000000000000000000000000000000000000000000000000000000000"\n' +
' ],\n' +
' "data": "0x00000000000000000000000000000000000000000000000023636b7d513f0000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb5",\n' +
' "removed": false\n' +
' },\n' +
' {\n' +
' "address": "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",\n' +
' "topics": [\n' +
' "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",\n' +
' "0x0000000000000000000000005c6006105b1b777a13d58d96393ad9e556882025",\n' +
' "0x0000000000000000000000001b46b72c5280f30fbe8a958b4f3c348fd0fd2e55"\n' +
' ],\n' +
' "data": "0x00000000000000000000000000000000000000000000011316d590258fba0000",\n' +
' "blockNumber": "0xccc493",\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57",\n' +
' "blockHash": "0x7e2661ac0e2f34dd2d6f449eea45aeec8470a0948af9daa33e684226640d819c",\n' +
' "logIndex": "0xb6",\n' +
' "removed": false\n' +
' }\n' +
' ],\n' +
' "transactionHash": "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec",\n' +
' "transactionIndex": "0x57"\n' +
' }'
def receipts = Mock(Reader) {
1 * it.read(TxId.from("0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec")) >>
Mono.just(new EthereumDirectReader.Result<>(receipt.getBytes(), null))
def logs = Mock(Reader) {
1 * it.read(BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da")) >>
Mono.just(new EthereumDirectReader.Result<>(logs, null))
}
def producer = new ProduceLogs(receipts, Chain.ETHEREUM__MAINNET)
def producer = new ProduceLogs(logs, Chain.ETHEREUM__MAINNET)
def update1 = new ConnectBlockUpdates.Update(
BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da"),
13412871,
ConnectBlockUpdates.UpdateType.NEW,
TxId.from("0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec"),
"upstream"
)
def update2 = new ConnectBlockUpdates.Update(
BlockId.from("0x668b92d6b8c7db1350fd527fec4885ce5be2159b2b7daf6b126babdcbaa349da"),
13412871,
ConnectBlockUpdates.UpdateType.DROP,
TxId.from("0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec"),
"upstream"
)
when:
@@ -376,9 +115,9 @@ class ProduceLogsSpec extends Specification {
.collectList().block(Duration.ofSeconds(1))
then:
act.size() == 3
act.size() == 1
act*.transactionHash.every { it.toHex() == "0xb5e554178a94fd993111f2ae64cb708cb0899d7b5182024e70d5c468164a8bec" }
act*.logIndex == [180, 181, 182]
act*.logIndex == [1]
act*.removed.every { it }
}
}