Track upstream finalization data (#499)

This commit is contained in:
Vyacheslav
2024-06-11 14:49:32 +03:00
committed by GitHub
parent 3e898cf325
commit effe881091
32 changed files with 933 additions and 144 deletions

View File

@@ -1,6 +1,10 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.BlockchainOuterClass.BlockTag
import io.emeraldpay.api.proto.BlockchainOuterClass.HeightSelector
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
import org.junit.jupiter.api.Assertions.assertEquals
@@ -53,6 +57,94 @@ class SelectorTest {
)
}
@ParameterizedTest
@MethodSource("finalData")
fun `sort with finalization`(
finalizationType: FinalizationType,
finalizationProto: BlockchainOuterClass.BlockTag,
) {
val up1 = mock<Upstream> {
on { getFinalizations() } doReturn listOf(FinalizationData(1L, finalizationType))
}
val up2 = mock<Upstream> {
on { getFinalizations() } doReturn listOf(FinalizationData(10L, finalizationType))
}
val up3 = mock<Upstream> {
on { getFinalizations() } doReturn listOf(FinalizationData(100L, finalizationType))
}
val up4 = mock<Upstream> {
on { getFinalizations() } doReturn listOf()
}
val ups = listOf(up4, up3, up2, up1)
val requestSelectors = listOf(
BlockchainOuterClass.Selector.newBuilder()
.setHeightSelector(
HeightSelector.newBuilder()
.setTag(finalizationProto),
)
.build(),
)
val upstreamFilter = Selector.convertToUpstreamFilter(requestSelectors)
val actual = ups.sortedWith(upstreamFilter.sort.comparator)
assertEquals(
listOf(up3, up2, up1, up4),
actual,
)
}
@Test
fun `sort with latest`() {
val mockHead1 = mock<Head> {
on { getCurrentHeight() } doReturn 1L
}
val up1 = mock<Upstream> {
on { getHead() } doReturn mockHead1
}
val mockHead2 = mock<Head> {
on { getCurrentHeight() } doReturn 2L
}
val up2 = mock<Upstream> {
on { getHead() } doReturn mockHead2
}
val mockHead3 = mock<Head> {
on { getCurrentHeight() } doReturn 3L
}
val up3 = mock<Upstream> {
on { getHead() } doReturn mockHead3
}
val mockHead4 = mock<Head> {
on { getCurrentHeight() } doReturn null
}
val up4 = mock<Upstream> {
on { getHead() } doReturn mockHead4
}
val ups = listOf(up2, up1, up4, up3)
val requestSelectors = listOf(
BlockchainOuterClass.Selector.newBuilder()
.setHeightSelector(
HeightSelector.newBuilder()
.setTag(BlockTag.LATEST),
)
.build(),
)
val upstreamFilter = Selector.convertToUpstreamFilter(requestSelectors)
val actual = ups.sortedWith(upstreamFilter.sort.comparator)
assertEquals(
listOf(up3, up2, up1, up4),
actual,
)
}
@Test
fun `preserve the same order if no lower bound type`() {
val up1 = mock<Upstream> {
@@ -127,5 +219,12 @@ class SelectorTest {
of(LowerBoundType.BLOCK, BlockchainOuterClass.LowerBoundType.LOWER_BOUND_BLOCK),
of(LowerBoundType.SLOT, BlockchainOuterClass.LowerBoundType.LOWER_BOUND_SLOT),
)
@JvmStatic
fun finalData(): List<Arguments> =
listOf(
of(FinalizationType.SAFE_BLOCK, BlockTag.SAFE),
of(FinalizationType.FINALIZED_BLOCK, BlockTag.FINALIZED),
)
}
}

View File

@@ -0,0 +1,75 @@
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.reader.ChainReader
import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.ChainResponse
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionRefJson
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import reactor.core.publisher.Mono
import java.time.Duration
import java.time.Instant
class EthereumFinalizationDetectorTest {
private lateinit var upstream: Upstream
private lateinit var chainReader: ChainReader
private lateinit var detector: EthereumFinalizationDetector
@BeforeEach
fun setUp() {
upstream = mock()
chainReader = mock()
`when`(upstream.getIngressReader()).thenReturn(chainReader)
detector = EthereumFinalizationDetector()
}
@Test
fun testDetectFinalization() {
`when`(chainReader.read(ChainRequest("eth_getBlockByNumber", ListParams("safe", false), 1)))
.thenReturn(
Mono.just(
ChainResponse(
Global.objectMapper.writeValueAsString(
BlockJson<TransactionRefJson>().apply {
number = 1
timestamp = Instant.now()
},
).toByteArray(),
null,
),
),
)
`when`(chainReader.read(ChainRequest("eth_getBlockByNumber", ListParams("finalized", false), 2)))
.thenReturn(
Mono.just(
ChainResponse(
Global.objectMapper.writeValueAsString(
BlockJson<TransactionRefJson>().apply {
number = 2
timestamp = Instant.now()
},
).toByteArray(),
null,
),
),
)
val flux = detector.detectFinalization(upstream, Duration.ofMillis(200))
flux.take(2).collectList().block()
val result = detector.getFinalizations().toList()
Assertions.assertEquals(2, result.size)
org.assertj.core.api.Assertions.assertThat(result)
.contains(FinalizationData(2L, FinalizationType.FINALIZED_BLOCK))
.contains(FinalizationData(1L, FinalizationType.SAFE_BLOCK))
}
}

View File

@@ -0,0 +1,125 @@
package io.emeraldpay.dshackle.upstream.grpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.test.MockGrpcServerKt
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
import io.grpc.stub.StreamObserver
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import reactor.core.publisher.Sinks
import reactor.core.scheduler.Schedulers
class GenericGrpcUpstreamTest {
private val parentId = "testParent"
private val hash: Byte = 0x01
private val role = UpstreamsConfig.UpstreamRole.PRIMARY
private val headSink = Sinks.many().multicast().directBestEffort<BlockchainOuterClass.ChainHead>()
private val remote =
MockGrpcServerKt().clientForServer(
object : BlockchainGrpc.BlockchainImplBase() {
override fun subscribeHead(
request: Common.Chain,
responseObserver: StreamObserver<BlockchainOuterClass.ChainHead>,
) {
Thread {
headSink.asFlux().subscribe { data ->
responseObserver.onNext(data)
Thread.sleep(500)
}
}.start()
}
},
)
private val client = Mockito.mock(JsonRpcGrpcClient::class.java)
private val nodeRating = 5
private val overrideLabels = Mockito.mock(UpstreamsConfig.Labels::class.java)
private val headScheduler = Schedulers.single()
private fun getUpstream(): GrpcUpstream {
return GenericGrpcUpstream(
parentId,
hash,
role,
Chain.LINEA__MAINNET,
remote,
client,
nodeRating,
overrideLabels,
ChainsConfig.ChainConfig.default(),
headScheduler,
)
}
@Test
fun start() {
val up = getUpstream()
up.getHead().start()
up.start()
headSink.emitNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setChain(Common.ChainRef.CHAIN_LINEA__MAINNET)
.setHeight(10L)
.setWeight(ByteString.EMPTY)
.setBlockId("a2622ec25e883dd13c1091c18ba717a1a794713baa77b8e68ec6a993045cb50f")
.setTimestamp(0)
.addAllFinalizationData(
mutableListOf(
Common
.FinalizationData
.newBuilder()
.setType(Common.FinalizationType.FINALIZATION_FINALIZED_BLOCK)
.setHeight(8L)
.build(),
),
)
.addAllLowerBounds(
mutableListOf(
BlockchainOuterClass.LowerBound
.newBuilder()
.setLowerBoundType(BlockchainOuterClass.LowerBoundType.LOWER_BOUND_TX)
.setLowerBoundTimestamp(0)
.setLowerBoundValue(1L).build(),
),
)
.build(),
) { _, _ ->
true
}
Thread.sleep(100)
Assertions.assertEquals(1, up.getFinalizations().size)
Assertions.assertTrue(
up.getFinalizations()
.contains(FinalizationData(8L, FinalizationType.FINALIZED_BLOCK)),
)
Assertions.assertTrue(
up.getLowerBounds()
.contains(LowerBoundData(1L, 0L, LowerBoundType.TX)),
)
}
@Test
fun getFinalizations() {
val up = getUpstream()
val finalizationData1 = FinalizationData(100L, FinalizationType.FINALIZED_BLOCK)
val finalizationData2 = FinalizationData(200L, FinalizationType.FINALIZED_BLOCK)
up.addFinalization(finalizationData1, "upstream1")
up.addFinalization(finalizationData2, "upstream2")
val finalizations = up.getFinalizations()
Assertions.assertEquals(1, finalizations.size)
Assertions.assertTrue(finalizations.contains(finalizationData2))
}
}