From 8b999d5439418a08f0a56bbef26c81ef3a015da9 Mon Sep 17 00:00:00 2001 From: Andrey Bronin Date: Wed, 9 Apr 2025 14:54:40 +0300 Subject: [PATCH] Kadena support (#650) * kadena support * add kadena mempool endpoits * createQuorumFor fix --- .../kotlin/chainsconfig.codegen.gradle.kts | 1 + emerald-grpc | 2 +- .../io/emeraldpay/dshackle/BlockchainType.kt | 3 +- foundation/src/main/resources/public | 2 +- .../dshackle/upstream/CallTargetsHolder.kt | 3 + .../upstream/calls/DefaultKadenaMethods.kt | 73 +++++++++++ .../upstream/generic/ChainSpecific.kt | 3 + .../upstream/kadena/KadenaChainSpecific.kt | 114 ++++++++++++++++++ .../kadena/KadenaLowerBoundService.kt | 15 +++ .../kadena/KadenaLowerBoundStateDetector.kt | 24 ++++ .../kadena/KadenaUpstreamSettingsDetector.kt | 44 +++++++ .../KadenaLowerBoundStateDetectorTest.kt | 23 ++++ 12 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultKadenaMethods.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaChainSpecific.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundService.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetector.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaUpstreamSettingsDetector.kt create mode 100644 src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt diff --git a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts index 19d80082..445d89cc 100644 --- a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts +++ b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts @@ -138,6 +138,7 @@ open class CodeGen(private val config: ChainsConfig) { "ton" -> "BlockchainType.TON" "cosmos" -> "BlockchainType.COSMOS" "ripple" -> "BlockchainType.RIPPLE" + "kadena" -> "BlockchainType.KADENA" else -> throw IllegalArgumentException("unknown blockchain type $type") } } diff --git a/emerald-grpc b/emerald-grpc index cea302c0..1b37beea 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit cea302c0d77d19a4d3be11ebce0f610a3806ea9a +Subproject commit 1b37beea67b19222cb4703276a4952dfa734dd64 diff --git a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt index eab4591e..1f10d9bc 100644 --- a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt @@ -13,7 +13,8 @@ enum class BlockchainType( ETHEREUM_BEACON_CHAIN(ApiType.REST), COSMOS(ApiType.JSON_RPC), TON(ApiType.REST), - RIPPLE(ApiType.JSON_RPC),; + RIPPLE(ApiType.JSON_RPC), + KADENA(ApiType.REST),; } enum class ApiType { diff --git a/foundation/src/main/resources/public b/foundation/src/main/resources/public index 88313c0c..cc548cfe 160000 --- a/foundation/src/main/resources/public +++ b/foundation/src/main/resources/public @@ -1 +1 @@ -Subproject commit 88313c0c55cce9bba47a6088ab7afbc1672ad915 +Subproject commit cc548cfe1907b7d899eadf5ec3b16cc3cf0fa4e7 diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt index 6a22675b..6e5e05c4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt @@ -4,6 +4,7 @@ import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.COSMOS import io.emeraldpay.dshackle.BlockchainType.ETHEREUM import io.emeraldpay.dshackle.BlockchainType.ETHEREUM_BEACON_CHAIN +import io.emeraldpay.dshackle.BlockchainType.KADENA import io.emeraldpay.dshackle.BlockchainType.NEAR import io.emeraldpay.dshackle.BlockchainType.POLKADOT import io.emeraldpay.dshackle.BlockchainType.RIPPLE @@ -19,6 +20,7 @@ import io.emeraldpay.dshackle.upstream.calls.DefaultBeaconChainMethods import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.DefaultCosmosMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods +import io.emeraldpay.dshackle.upstream.calls.DefaultKadenaMethods import io.emeraldpay.dshackle.upstream.calls.DefaultNearMethods import io.emeraldpay.dshackle.upstream.calls.DefaultPolkadotMethods import io.emeraldpay.dshackle.upstream.calls.DefaultRippleMethods @@ -54,6 +56,7 @@ class CallTargetsHolder { COSMOS -> DefaultCosmosMethods() TON -> DefaultTonHttpMethods(connection) RIPPLE -> DefaultRippleMethods() + KADENA -> DefaultKadenaMethods() UNKNOWN -> throw IllegalArgumentException("unknown chain") } callTargets[chain] = created diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultKadenaMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultKadenaMethods.kt new file mode 100644 index 00000000..d732b41c --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultKadenaMethods.kt @@ -0,0 +1,73 @@ +package io.emeraldpay.dshackle.upstream.calls + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import io.emeraldpay.dshackle.quorum.BroadcastQuorum +import io.emeraldpay.dshackle.quorum.CallQuorum +import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException + +class DefaultKadenaMethods : CallMethods { + + private val kadenaMethods = setOf( + getMethod("/chain/*/hash"), + postMethod("/chain/*/hash"), + getMethod("/chain/*/header"), + getMethod("/chain/*/header/*"), + postMethod("/chain/*/header/branch"), + getMethod("/chain/*/payload/*"), + postMethod("/chain/*/payload/batch"), + getMethod("/chain/*/payload/*/outputs"), + postMethod("/chain/*/payload/*/outputs/batch"), + getMethod("/chain/*/payload/*"), + postMethod("/chain/*/pact/local"), + postMethod("/chain/*/pact/send"), + postMethod("/chain/*/pact/poll"), + postMethod("/chain/*/pact/listen"), + postMethod("/chain/*/pact/private"), + postMethod("/chain/*/pact/spv"), + postMethod("/chain/*/mempool/getPending"), + postMethod("/chain/*/mempool/member"), + postMethod("/chain/*/mempool/lookup"), + ) + + private val insert = setOf( + putMethod("/chain/*/mempool/insert"), + ) + + private val allowedMethods: Set = kadenaMethods + insert + + override fun createQuorumFor(method: String): CallQuorum { + if (insert.contains(method)) { + return BroadcastQuorum() + } + return AlwaysQuorum() + } + + override fun isCallable(method: String): Boolean { + return allowedMethods.contains(method) + } + + override fun getSupportedMethods(): Set { + return allowedMethods.toSortedSet() + } + + override fun isHardcoded(method: String): Boolean { + return false + } + + override fun executeHardcoded(method: String): ByteArray { + throw RpcException(-32601, "Method not found") + } + + override fun getGroupMethods(groupName: String): Set { + return when (groupName) { + "default" -> getSupportedMethods() + else -> emptyList() + }.toSet() + } + + private fun getMethod(method: String) = "GET#$method" + + private fun postMethod(method: String) = "POST#$method" + + private fun putMethod(method: String) = "PUT#$method" +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt index 65ae2d13..cdd4e19f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt @@ -4,6 +4,7 @@ import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.COSMOS import io.emeraldpay.dshackle.BlockchainType.ETHEREUM import io.emeraldpay.dshackle.BlockchainType.ETHEREUM_BEACON_CHAIN +import io.emeraldpay.dshackle.BlockchainType.KADENA import io.emeraldpay.dshackle.BlockchainType.NEAR import io.emeraldpay.dshackle.BlockchainType.POLKADOT import io.emeraldpay.dshackle.BlockchainType.RIPPLE @@ -38,6 +39,7 @@ import io.emeraldpay.dshackle.upstream.cosmos.CosmosChainSpecific import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions import io.emeraldpay.dshackle.upstream.finalization.FinalizationDetector +import io.emeraldpay.dshackle.upstream.kadena.KadenaChainSpecific import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService import io.emeraldpay.dshackle.upstream.near.NearChainSpecific import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific @@ -115,6 +117,7 @@ object ChainSpecificRegistry { TON -> TonHttpSpecific COSMOS -> CosmosChainSpecific RIPPLE -> RippleChainSpecific + KADENA -> KadenaChainSpecific BITCOIN -> throw IllegalArgumentException("bitcoin should use custom streams implementation") UNKNOWN -> throw IllegalArgumentException("unknown chain") } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaChainSpecific.kt new file mode 100644 index 00000000..b4895b69 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaChainSpecific.kt @@ -0,0 +1,114 @@ +package io.emeraldpay.dshackle.upstream.kadena + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.Global +import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.foundation.ChainOptions.Options +import io.emeraldpay.dshackle.reader.ChainReader +import io.emeraldpay.dshackle.upstream.ChainRequest +import io.emeraldpay.dshackle.upstream.GenericSingleCallValidator +import io.emeraldpay.dshackle.upstream.SingleValidator +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.emeraldpay.dshackle.upstream.UpstreamSettingsDetector +import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult +import io.emeraldpay.dshackle.upstream.generic.AbstractPollChainSpecific +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService +import io.emeraldpay.dshackle.upstream.rpcclient.RestParams +import reactor.core.publisher.Mono +import java.math.BigInteger +import java.time.Instant +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +object KadenaChainSpecific : AbstractPollChainSpecific() { + @OptIn(ExperimentalEncodingApi::class) + override fun parseBlock(data: ByteArray, upstreamId: String, api: ChainReader): Mono { + val block = Global.objectMapper.readValue(data) + + val blockHash = Base64.encode(block.id.encodeToByteArray()) + return Mono.just( + BlockContainer( + height = block.height, + hash = BlockId.fromBase64(blockHash), + difficulty = BigInteger.ZERO, + timestamp = Instant.EPOCH, + full = false, + json = data, + parsed = block, + transactions = emptyList(), + upstreamId = upstreamId, + parentHash = BlockId.fromBase64(blockHash), + ), + ) + } + + override fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono { + throw NotImplementedError() + } + + override fun listenNewHeadsRequest(): ChainRequest { + throw NotImplementedError() + } + + override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest { + throw NotImplementedError() + } + + override fun upstreamValidators( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): List> { + var validators = listOf( + GenericSingleCallValidator( + ChainRequest("GET#/cut", RestParams.emptyParams()), + upstream, + ) { data -> + val block = Global.objectMapper.readValue(data) + if (block.id != "") { + UpstreamAvailability.OK + } else { + UpstreamAvailability.UNAVAILABLE + } + }, + ) + + return validators + } + + override fun upstreamSettingsValidators( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): List> { + return emptyList() + } + + override fun lowerBoundService(chain: Chain, upstream: Upstream): LowerBoundService { + return KadenaLowerBoundService(chain, upstream) + } + + override fun upstreamSettingsDetector(chain: Chain, upstream: Upstream): UpstreamSettingsDetector { + return KadenaUpstreamSettingsDetector(upstream) + } + + override fun latestBlockRequest(): ChainRequest { + return ChainRequest("GET#/cut", RestParams.emptyParams()) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class KadenaHeader( + @JsonProperty("height") var height: Long, + @JsonProperty("weight") var weight: String, + @JsonProperty("instance") var instance: String, + @JsonProperty("id") var id: String, +) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundService.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundService.kt new file mode 100644 index 00000000..007e9370 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundService.kt @@ -0,0 +1,15 @@ +package io.emeraldpay.dshackle.upstream.kadena + +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundDetector +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService + +class KadenaLowerBoundService( + private val chain: Chain, + upstream: Upstream, +) : LowerBoundService(chain, upstream) { + override fun detectors(): List { + return listOf(KadenaLowerBoundStateDetector(chain)) + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetector.kt new file mode 100644 index 00000000..445ad6d2 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetector.kt @@ -0,0 +1,24 @@ +package io.emeraldpay.dshackle.upstream.kadena + +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundDetector +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import reactor.core.publisher.Flux + +class KadenaLowerBoundStateDetector( + chain: Chain, +) : LowerBoundDetector(chain) { + + override fun period(): Long { + return 120 + } + + override fun internalDetectLowerBound(): Flux { + return Flux.just(LowerBoundData(1, LowerBoundType.STATE)) + } + + override fun types(): Set { + return setOf(LowerBoundType.STATE) + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaUpstreamSettingsDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaUpstreamSettingsDetector.kt new file mode 100644 index 00000000..0463f35f --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaUpstreamSettingsDetector.kt @@ -0,0 +1,44 @@ +package io.emeraldpay.dshackle.upstream.kadena + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.readValue +import io.emeraldpay.dshackle.Global +import io.emeraldpay.dshackle.upstream.BasicUpstreamSettingsDetector +import io.emeraldpay.dshackle.upstream.ChainRequest +import io.emeraldpay.dshackle.upstream.NodeTypeRequest +import io.emeraldpay.dshackle.upstream.UNKNOWN_CLIENT_VERSION +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.RestParams +import reactor.core.publisher.Flux + +class KadenaUpstreamSettingsDetector( + upstream: Upstream, +) : BasicUpstreamSettingsDetector(upstream) { + override fun internalDetectLabels(): Flux> { + return Flux.merge( + detectNodeType(), + ) + } + + override fun clientVersionRequest(): ChainRequest { + return ChainRequest("GET#/cut", RestParams.emptyParams()) + } + + override fun parseClientVersion(data: ByteArray): String { + return Global.objectMapper.readValue(data).instance + } + + @JsonIgnoreProperties(ignoreUnknown = true) + data class KadenaHeader( + @JsonProperty("instance") var instance: String, + ) + + override fun nodeTypeRequest(): NodeTypeRequest = NodeTypeRequest(clientVersionRequest()) + + override fun clientType(node: JsonNode): String = "kadena" + + override fun clientVersion(node: JsonNode): String = + node.get("version")?.get("version")?.asText() ?: UNKNOWN_CLIENT_VERSION +} diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt new file mode 100644 index 00000000..6217fbc8 --- /dev/null +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/kadena/KadenaLowerBoundStateDetectorTest.kt @@ -0,0 +1,23 @@ +package io.emeraldpay.dshackle.upstream.kadena + +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import org.junit.jupiter.api.Test +import reactor.test.StepVerifier +import java.time.Duration + +class KadenaLowerBoundStateDetectorTest { + + @Test + fun `kadena lower block is 1`() { + val detector = KadenaLowerBoundStateDetector(Chain.KADENA__MAINNET) + + StepVerifier.withVirtualTime { detector.detectLowerBound() } + .expectSubscription() + .expectNoEvent(Duration.ofSeconds(15)) + .expectNext(LowerBoundData(1, LowerBoundType.STATE)) + .thenCancel() + .verify(Duration.ofSeconds(3)) + } +}