From 68114bcb65da1bcbf00622e9edc79e5e98e4e64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB?= Date: Tue, 5 Nov 2024 15:19:42 +0400 Subject: [PATCH] Ton chain id validator --- .../upstream/ton/TonChainIdValidator.kt | 82 ++++++++++ .../dshackle/upstream/ton/TonHttpSpecific.kt | 15 +- .../upstream/ton/TonChainIdValidatorTest.kt | 143 ++++++++++++++++++ .../blocks/ton/masterchain-info.json | 24 +++ .../ton/ton-block-missed-global-id.json | 3 + .../resources/blocks/ton/ton-block-wrong.json | 15 ++ src/test/resources/blocks/ton/ton-block.json | 15 ++ 7 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidator.kt create mode 100644 src/test/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidatorTest.kt create mode 100644 src/test/resources/blocks/ton/masterchain-info.json create mode 100644 src/test/resources/blocks/ton/ton-block-missed-global-id.json create mode 100644 src/test/resources/blocks/ton/ton-block-wrong.json create mode 100644 src/test/resources/blocks/ton/ton-block.json diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidator.kt new file mode 100644 index 000000000..c8506a04b --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidator.kt @@ -0,0 +1,82 @@ +package io.emeraldpay.dshackle.upstream.ton + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.readValue +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.Global +import io.emeraldpay.dshackle.upstream.ChainRequest +import io.emeraldpay.dshackle.upstream.ChainResponse +import io.emeraldpay.dshackle.upstream.SingleValidator +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult +import io.emeraldpay.dshackle.upstream.rpcclient.RestParams +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono + +class TonChainIdValidator( + private val upstream: Upstream, + private val chain: Chain, +) : SingleValidator { + + companion object { + private val log: Logger = LoggerFactory.getLogger(TonChainIdValidator::class.java) + } + + override fun validate(onError: ValidateUpstreamSettingsResult): Mono { + return upstream.getIngressReader() + .read(TonHttpSpecific.latestBlockRequest()) + .flatMap(ChainResponse::requireResult) + .flatMap { + val tonMasterchainInfo = Global.objectMapper.readValue(it) + + upstream.getIngressReader() + .read( + ChainRequest( + "GET#/getBlockHeader", + RestParams( + emptyList(), + listOf( + "workchain" to tonMasterchainInfo.result.last.workchain.toString(), + "shard" to tonMasterchainInfo.result.last.shard, + "seqno" to tonMasterchainInfo.result.last.seqno.toString(), + ), + emptyList(), + ByteArray(0), + ), + ), + ) + .flatMap(ChainResponse::requireResult) + .flatMap { headerResponse -> + val blockHeader = Global.objectMapper.readValue(headerResponse) + + val globalId = blockHeader.get("result")?.get("global_id")?.asText() + if (globalId == null) { + log.warn( + "Couldn't receive global_id from the block header of upstream {} for workchain {}, shard {}, seqno {}", + upstream.getId(), + tonMasterchainInfo.result.last.workchain, + tonMasterchainInfo.result.last.shard, + tonMasterchainInfo.result.last.seqno, + ) + Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR) + } else { + if (globalId == chain.chainId) { + Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID) + } else { + log.warn( + "Wrong chainId {} for {}, expected {}", + globalId, + chain, + chain.chainId, + ) + Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR) + } + } + } + }.onErrorResume { + log.error("Error during chain validation of upstream {}, reason - {}", upstream.getId(), it.message) + Mono.just(onError) + } + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonHttpSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonHttpSpecific.kt index c6da05195..6ed79ed38 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonHttpSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ton/TonHttpSpecific.kt @@ -87,7 +87,20 @@ object TonHttpSpecific : AbstractPollChainSpecific() { config: ChainConfig, ): List> { // add check generic block - return emptyList() + return listOf( + TonChainIdValidator(upstream, chain), + ) + } + + override fun chainSettingsValidator( + chain: Chain, + upstream: Upstream, + reader: ChainReader?, + ): SingleValidator? { + if (upstream.getOptions().disableUpstreamValidation) { + return null + } + return TonChainIdValidator(upstream, chain) } } diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidatorTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidatorTest.kt new file mode 100644 index 000000000..bbb1c1b24 --- /dev/null +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/ton/TonChainIdValidatorTest.kt @@ -0,0 +1,143 @@ +package io.emeraldpay.dshackle.upstream.ton + +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.reader.ChainReader +import io.emeraldpay.dshackle.upstream.ChainCallError +import io.emeraldpay.dshackle.upstream.ChainRequest +import io.emeraldpay.dshackle.upstream.ChainResponse +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult +import io.emeraldpay.dshackle.upstream.rpcclient.RestParams +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import java.time.Duration + +class TonChainIdValidatorTest { + + @ParameterizedTest + @MethodSource("data") + fun `validate ton chainId`( + blockFilePath: String, + expectedResult: ValidateUpstreamSettingsResult, + ) { + val masterchainInfoBytes = this::class.java.getResource("/blocks/ton/masterchain-info.json")!!.readBytes() + val block = this::class.java.getResource(blockFilePath)!!.readBytes() + val blockReq = ChainRequest( + "GET#/getBlockHeader", + RestParams( + emptyList(), + listOf( + "workchain" to "-1", + "shard" to "-9223372036854775808", + "seqno" to "41689287", + ), + emptyList(), + ByteArray(0), + ), + ) + val reader = mock { + on { read(ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams())) } doReturn + Mono.just(ChainResponse(masterchainInfoBytes, null)) + on { read(blockReq) } doReturn Mono.just(ChainResponse(block, null)) + } + val upstream = mock { + on { getIngressReader() } doReturn reader + } + val validator = TonChainIdValidator(upstream, Chain.TON__MAINNET) + + StepVerifier.create(validator.validate(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR)) + .expectSubscription() + .expectNext(expectedResult) + .expectComplete() + .verify(Duration.ofSeconds(1)) + + verify(reader).read(ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams())) + verify(reader).read(blockReq) + } + + @Test + fun `setting error if couldn't read from upstream getMasterchainInfo`() { + val reader = mock { + on { read(ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams())) } doReturn + Mono.just(ChainResponse(null, ChainCallError(1, "Big error"))) + } + val upstream = mock { + on { getIngressReader() } doReturn reader + } + val validator = TonChainIdValidator(upstream, Chain.TON__MAINNET) + + StepVerifier.create(validator.validate(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR)) + .expectSubscription() + .expectNext(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR) + .expectComplete() + .verify(Duration.ofSeconds(1)) + + verify(reader).read(ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams())) + verify(reader, never()).read(argThat { method == "GET#/getBlockHeader" }) + } + + @Test + fun `setting error if couldn't read from upstream getBlockHeader`() { + val masterchainInfoBytes = this::class.java.getResource("/blocks/ton/masterchain-info.json")!!.readBytes() + val blockReq = ChainRequest( + "GET#/getBlockHeader", + RestParams( + emptyList(), + listOf( + "workchain" to "-1", + "shard" to "-9223372036854775808", + "seqno" to "41689287", + ), + emptyList(), + ByteArray(0), + ), + ) + val reader = mock { + on { read(ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams())) } doReturn + Mono.just(ChainResponse(masterchainInfoBytes, null)) + on { read(blockReq) } doReturn Mono.just(ChainResponse(null, ChainCallError(1, "Super error"))) + } + val upstream = mock { + on { getIngressReader() } doReturn reader + } + val validator = TonChainIdValidator(upstream, Chain.TON__MAINNET) + + StepVerifier.create(validator.validate(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR)) + .expectSubscription() + .expectNext(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR) + .expectComplete() + .verify(Duration.ofSeconds(1)) + + verify(reader).read(ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams())) + verify(reader).read(blockReq) + } + + companion object { + @JvmStatic + fun data(): List { + return listOf( + Arguments.of( + "/blocks/ton/ton-block.json", + ValidateUpstreamSettingsResult.UPSTREAM_VALID, + ), + Arguments.of( + "/blocks/ton/ton-block-wrong.json", + ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR, + ), + Arguments.of( + "/blocks/ton/ton-block-missed-global-id.json", + ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR, + ), + ) + } + } +} diff --git a/src/test/resources/blocks/ton/masterchain-info.json b/src/test/resources/blocks/ton/masterchain-info.json new file mode 100644 index 000000000..86ba5c171 --- /dev/null +++ b/src/test/resources/blocks/ton/masterchain-info.json @@ -0,0 +1,24 @@ +{ + "ok": true, + "result": { + "@type": "blocks.masterchainInfo", + "last": { + "@type": "ton.blockIdExt", + "workchain": -1, + "shard": "-9223372036854775808", + "seqno": 41689287, + "root_hash": "kOdzFs2iOVrpBVpD5Wt1o9Z+eTJIjGXxMurjuKiIJgg=", + "file_hash": "2gGZ8/7b8ztvtzPaDbzsWzxvCSvwMoct1v2l9jtV2G0=" + }, + "state_root_hash": "0mgSkvXs8QIO94ol3bxKxe0Jcu34eaB+/anKRgBkuQI=", + "init": { + "@type": "ton.blockIdExt", + "workchain": -1, + "shard": "0", + "seqno": 0, + "root_hash": "F6OpKZKqvqeFp6CQmFomXNMfMj2EnaUSOXN+Mh+wVWk=", + "file_hash": "XplPz01CXAps5qeSWUtxcyBfdAo5zVb1N979KLSKD24=" + }, + "@extra": "1730733199.581394:6:0.46521772491020763" + } +} \ No newline at end of file diff --git a/src/test/resources/blocks/ton/ton-block-missed-global-id.json b/src/test/resources/blocks/ton/ton-block-missed-global-id.json new file mode 100644 index 000000000..8cba4747d --- /dev/null +++ b/src/test/resources/blocks/ton/ton-block-missed-global-id.json @@ -0,0 +1,3 @@ +{ + "ok": false +} \ No newline at end of file diff --git a/src/test/resources/blocks/ton/ton-block-wrong.json b/src/test/resources/blocks/ton/ton-block-wrong.json new file mode 100644 index 000000000..78f9117c9 --- /dev/null +++ b/src/test/resources/blocks/ton/ton-block-wrong.json @@ -0,0 +1,15 @@ +{ + "ok": true, + "result": { + "@type": "blocks.header", + "id": { + "@type": "ton.blockIdExt", + "workchain": -1, + "shard": "-9223372036854775808", + "seqno": 41689287, + "root_hash": "kOdzFs2iOVrpBVpD5Wt1o9Z+eTJIjGXxMurjuKiIJgg=", + "file_hash": "2gGZ8/7b8ztvtzPaDbzsWzxvCSvwMoct1v2l9jtV2G0=" + }, + "global_id": -3345 + } +} \ No newline at end of file diff --git a/src/test/resources/blocks/ton/ton-block.json b/src/test/resources/blocks/ton/ton-block.json new file mode 100644 index 000000000..2d325d290 --- /dev/null +++ b/src/test/resources/blocks/ton/ton-block.json @@ -0,0 +1,15 @@ +{ + "ok": true, + "result": { + "@type": "blocks.header", + "id": { + "@type": "ton.blockIdExt", + "workchain": -1, + "shard": "-9223372036854775808", + "seqno": 41689287, + "root_hash": "kOdzFs2iOVrpBVpD5Wt1o9Z+eTJIjGXxMurjuKiIJgg=", + "file_hash": "2gGZ8/7b8ztvtzPaDbzsWzxvCSvwMoct1v2l9jtV2G0=" + }, + "global_id": -239 + } +} \ No newline at end of file