Skip to content

Commit

Permalink
Ton chain id validator
Browse files Browse the repository at this point in the history
  • Loading branch information
Кирилл committed Nov 7, 2024
1 parent b9b8e68 commit 68114bc
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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<ValidateUpstreamSettingsResult> {

companion object {
private val log: Logger = LoggerFactory.getLogger(TonChainIdValidator::class.java)
}

override fun validate(onError: ValidateUpstreamSettingsResult): Mono<ValidateUpstreamSettingsResult> {
return upstream.getIngressReader()
.read(TonHttpSpecific.latestBlockRequest())
.flatMap(ChainResponse::requireResult)
.flatMap {
val tonMasterchainInfo = Global.objectMapper.readValue<TonMasterchainInfo>(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<JsonNode>(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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,20 @@ object TonHttpSpecific : AbstractPollChainSpecific() {
config: ChainConfig,
): List<SingleValidator<ValidateUpstreamSettingsResult>> {
// add check generic block
return emptyList()
return listOf(
TonChainIdValidator(upstream, chain),
)
}

override fun chainSettingsValidator(
chain: Chain,
upstream: Upstream,
reader: ChainReader?,
): SingleValidator<ValidateUpstreamSettingsResult>? {
if (upstream.getOptions().disableUpstreamValidation) {
return null
}
return TonChainIdValidator(upstream, chain)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ChainReader> {
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<Upstream> {
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<ChainReader> {
on { read(ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams())) } doReturn
Mono.just(ChainResponse(null, ChainCallError(1, "Big error")))
}
val upstream = mock<Upstream> {
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<ChainReader> {
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<Upstream> {
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<Arguments> {
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,
),
)
}
}
}
24 changes: 24 additions & 0 deletions src/test/resources/blocks/ton/masterchain-info.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions src/test/resources/blocks/ton/ton-block-missed-global-id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"ok": false
}
15 changes: 15 additions & 0 deletions src/test/resources/blocks/ton/ton-block-wrong.json
Original file line number Diff line number Diff line change
@@ -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
}
}
15 changes: 15 additions & 0 deletions src/test/resources/blocks/ton/ton-block.json
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 68114bc

Please sign in to comment.