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

@@ -108,7 +108,7 @@ class ApiReaderMock implements Reader<ChainRequest, ChainResponse> {
}
error = new ChainCallError(-32601, "Method ${request.method} with ${request.params} is not mocked")
}
return new ChainResponse(result, error, ChainResponse.Id.from(request.id), null, null, null)
return new ChainResponse(result, error, ChainResponse.Id.from(request.id), null, null, null, null)
} as Callable<ChainResponse>
return Mono.fromCallable(call)
}

View File

@@ -77,6 +77,7 @@ class GenericUpstreamMock extends GenericUpstream {
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&validator,
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&upstreamSettingsDetector,
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&lowerBoundService,
io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific.INSTANCE.&finalizationDetectorBuilder,
)
this.ethereumHeadMock = this.getHead() as EthereumHeadMock
setLag(0)

View File

@@ -77,7 +77,8 @@ class FilteredApisSpec extends Specification {
connectorFactory,
cs.&validator,
cs.&upstreamSettingsDetector,
cs.&lowerBoundService
cs.&lowerBoundService,
cs.&finalizationDetectorBuilder
)
}
def matcher = new Selector.LabelMatcher("test", ["foo"])

View File

@@ -12,6 +12,7 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import io.emeraldpay.dshackle.upstream.ethereum.domain.Address
import io.emeraldpay.dshackle.upstream.ethereum.domain.BlockHash
@@ -33,6 +34,40 @@ class EthereumDirectReaderSpec extends Specification {
String hash1 = "0x40d15edaff9acdabd2a1c96fd5f683b3300aad34e7015f34def3c56ba8a7ffb5"
String address1 = "0xe0aadb0a012dbcdc529c4c743d3e0385a0b54d3d"
Upstream.UpstreamSettingsData data = new Upstream.UpstreamSettingsData("test")
def "Reads block by finalization"() {
setup:
def json = new BlockJson().tap {
number = 100
hash = BlockHash.from(hash1)
timestamp = Instant.now()
totalDifficulty = BigInteger.ONE
parentHash = BlockHash.from(hash1)
transactions = []
}
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.requestReaderFactory = Mock(RequestReaderFactory) {
1 * create({ it.upstreamFilter.sort == Selector.Sort.safe }) >> Mock(RequestReader) {
1 * read(new ChainRequest("eth_getBlockByNumber", new ListParams(["safe", false]))) >> Mono.just(
new RequestReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, data, null)
)
}
}
when:
def act = reader.blockByFinalizationReader.read(FinalizationType.SAFE_BLOCK)
then:
StepVerifier.create(act)
.expectNextMatches { block ->
block.data.hash.toHexWithPrefix() == hash1
}
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Reads block by hash"() {
setup:

View File

@@ -10,6 +10,8 @@ import io.emeraldpay.dshackle.upstream.EmptyHead
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.finalization.FinalizationData
import io.emeraldpay.dshackle.upstream.finalization.FinalizationType
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import org.apache.commons.collections4.functors.ConstantFactory
@@ -84,8 +86,8 @@ class EthereumLocalReaderSpec extends Specification {
then:
act != null
with(act.block()) {
it.first.length > 0
with(Global.objectMapper.readValue(it.first, BlockJson)) {
it.result.length > 0
with(Global.objectMapper.readValue(it.result, BlockJson)) {
number == 101
}
}
@@ -112,8 +114,8 @@ class EthereumLocalReaderSpec extends Specification {
then:
act != null
with(act.block()) {
it.first.length > 0
with(Global.objectMapper.readValue(it.first, BlockJson)) {
it.result.length > 0
with(Global.objectMapper.readValue(it.result, BlockJson)) {
number == 0
}
}
@@ -140,13 +142,43 @@ class EthereumLocalReaderSpec extends Specification {
then:
act != null
with(act.block()) {
it.first.length > 0
with(Global.objectMapper.readValue(it.first, BlockJson)) {
it.result.length > 0
with(Global.objectMapper.readValue(it.result, BlockJson)) {
number == 74735
}
}
}
def "getBlockByNumber fetches the block by tag"() {
setup:
def head = Stub(Head) {}
def reader = Mock(EthereumCachingReader) {
1 * blockByFinalization() >> Mock(Reader) {
1 * read(FinalizationType.SAFE_BLOCK) >> Mono.just(
new EthereumDirectReader.Result<>(TestingCommons.blockForEthereum(74735L), null)
)
}
}
def methods = new DefaultEthereumMethods(Chain.ETHEREUM__MAINNET, false)
def router = new EthereumLocalReader(reader, methods, head, null)
when:
def act = router.read(
new ChainRequest("eth_getBlockByNumber",
new ListParams("safe", false))
)
then:
act != null
with(act.block()) {
it.result.length > 0
with(Global.objectMapper.readValue(it.result, BlockJson)) {
number == 74735
}
it.finalization == new FinalizationData(74735, FinalizationType.SAFE_BLOCK)
}
}
def "getBlockByNumber skips requests with tx bodies"() {
setup:
def head = Mock(Head)

View File

@@ -60,7 +60,7 @@ class GrpcHeadSpec extends Specification {
}
})
def convert = { BlockchainOuterClass.ChainHead head ->
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of())
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of(), List.of())
}
def head = new GrpcHead(
"test",
@@ -127,7 +127,7 @@ class GrpcHeadSpec extends Specification {
}
})
def convert = { BlockchainOuterClass.ChainHead head ->
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of())
new GrpcHead.GrpcHeadData(TestingCommons.blockForBitcoin(head.height), List.of(), List.of())
}
def head = new GrpcHead(
"test",

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))
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2019 ETCDEV GmbH
* 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.test
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.grpc.inprocess.InProcessChannelBuilder
import io.grpc.inprocess.InProcessServerBuilder
import io.grpc.testing.GrpcCleanupRule
import org.junit.Rule
class MockGrpcServerKt {
@get:Rule
val grpcCleanup = GrpcCleanupRule()
fun clientForServer(impl: BlockchainGrpc.BlockchainImplBase): ReactorBlockchainGrpc.ReactorBlockchainStub {
val serverName = InProcessServerBuilder.generateName()
grpcCleanup.register(
InProcessServerBuilder
.forName(serverName).directExecutor().addService(impl).build().start(),
)
val channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build())
return ReactorBlockchainGrpc.newReactorStub(channel)
}
}