Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add cosmos rpc support #492

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ open class CodeGen(private val config: ChainsConfig) {
"solana" -> "BlockchainType.SOLANA"
"near" -> "BlockchainType.NEAR"
"eth-beacon-chain" -> "BlockchainType.ETHEREUM_BEACON_CHAIN"
"cosmos" -> "BlockchainType.COSMOS"
else -> throw IllegalArgumentException("unknown blockchain type $type")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ enum class BlockchainType(
POLKADOT(ApiType.JSON_RPC),
SOLANA(ApiType.JSON_RPC),
NEAR(ApiType.JSON_RPC),
ETHEREUM_BEACON_CHAIN(ApiType.REST);
ETHEREUM_BEACON_CHAIN(ApiType.REST),
COSMOS(ApiType.JSON_RPC);
}

enum class ApiType {
Expand Down
25 changes: 24 additions & 1 deletion foundation/src/main/resources/chains.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ chain-settings:
lags:
syncing: 6
lagging: 1
fork-choice: quorum
chains:
- id: Mainnet
chain-id: 0x1
Expand Down Expand Up @@ -182,7 +183,7 @@ chain-settings:
lags:
syncing: 20
lagging: 10
fork-choice: quorum
fork-choice: height
chains:
- id: Mainnet
priority: 100
Expand Down Expand Up @@ -1403,3 +1404,25 @@ chain-settings:
grpcId: 1055
short-names: [real]
chain-id: 0x1b254
- id: cosmos-hub
label: Cosmos Hub
type: cosmos
settings:
expected-block-time: 6s
lags:
syncing: 6
lagging: 1
chains:
- id: Mainnet
chain-id: 0x0
short-names: [ cosmos-hub ]
code: COSMOS_HUB
grpcId: 1057
priority: 100
- id: Testnet
chain-id: 0x0
short-names: [ cosmos-hub-testnet ]
code: COSMOS_HUB_TESTNET
grpcId: 10079
priority: 10

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.emeraldpay.dshackle.upstream

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.NEAR
Expand All @@ -12,6 +13,7 @@ import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.upstream.calls.CallMethods
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.DefaultPolkadotMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultStarknetMethods
Expand All @@ -34,6 +36,7 @@ class CallTargetsHolder {
SOLANA -> DefaultSolanaMethods()
NEAR -> DefaultNearMethods()
ETHEREUM_BEACON_CHAIN -> DefaultBeaconChainMethods()
COSMOS -> DefaultCosmosMethods()
UNKNOWN -> throw IllegalArgumentException("unknown chain")
}
callTargets[chain] = created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.reader.ChainReader
import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamSettingsDetector
import io.emeraldpay.dshackle.upstream.UpstreamValidator
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

object BeaconChainSpecific : AbstractPollChainSpecific() {
override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer {
override fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono<BlockContainer> {
throw NotImplementedError()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 DefaultCosmosMethods : CallMethods {
private val all = setOf(
"health",
"status",
"net_info",
"blockchain",
"block",
"block_by_hash",
"block_results",
"commit",
"validators",
"genesis",
"genesis_chunked",
// "dump_consensus_state", // not safe
// "consensus_state", // not safe
"consensus_params",
"unconfirmed_txs",
"num_unconfirmed_txs",
"tx_search",
"block_search",
"tx",
"check_tx",
"abci_info",
"abci_query",
)

private val add = setOf(
"broadcast_evidence",
"broadcast_tx_sync",
"broadcast_tx_async",
"broadcast_tx_commit",
)

private val allowedMethods: Set<String> = all + add

override fun createQuorumFor(method: String): CallQuorum {
if (add.contains(method)) {
return BroadcastQuorum()
}
return AlwaysQuorum()
}

override fun isCallable(method: String): Boolean {
return allowedMethods.contains(method)
}

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<String> =
when (groupName) {
"default" -> getSupportedMethods()
else -> emptyList()
}.toSet()

override fun getSupportedMethods(): Set<String> {
return allowedMethods.toSortedSet()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package io.emeraldpay.dshackle.upstream.cosmos

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
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.SingleCallValidator
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability.OK
import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.generic.AbstractPollChainSpecific
import io.emeraldpay.dshackle.upstream.generic.GenericUpstreamValidator
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import io.emeraldpay.dshackle.upstream.rpcclient.ObjectParams
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.time.Instant

object CosmosChainSpecific : AbstractPollChainSpecific() {
override fun latestBlockRequest(): ChainRequest = ChainRequest("block", ObjectParams())

override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer {
val result = Global.objectMapper.readValue(data, CosmosBlockResult::class.java)

return BlockContainer(
height = result.block.header.height.toLong(),
hash = BlockId.from(result.blockId.hash),
difficulty = BigInteger.ZERO,
timestamp = result.block.header.time,
full = false,
json = data,
parsed = result,
transactions = emptyList(),
upstreamId = upstreamId,
parentHash = BlockId.from(result.block.header.lastBlockId.hash),
)
}

override fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono<BlockContainer> {
val event = Global.objectMapper.readValue(data, CosmosBlockEvent::class.java)

return api.read(ChainRequest("block", ObjectParams("height" to event.data.value.header.height))).flatMap {
val blockData = it.getResult()
val result = Global.objectMapper.readValue(blockData, CosmosBlockResult::class.java)
Mono.just(
BlockContainer(
height = result.block.header.height.toLong(),
hash = BlockId.from(result.blockId.hash),
difficulty = BigInteger.ZERO,
timestamp = result.block.header.time,
full = false,
json = blockData,
parsed = result,
transactions = emptyList(),
upstreamId = upstreamId,
parentHash = BlockId.from(result.block.header.lastBlockId.hash),
),
)
}
}

override fun listenNewHeadsRequest() = throw NotImplementedError()
// ChainRequest("subscribe", ListParams("tm.event = 'NewBlockHeader'"))

override fun unsubscribeNewHeadsRequest(subId: String) = throw NotImplementedError()
// ChainRequest("unsubscribe", ListParams("tm.event = 'NewBlockHeader'"))

override fun validator(chain: Chain, upstream: Upstream, options: Options, config: ChainConfig): UpstreamValidator {
return GenericUpstreamValidator(
upstream,
options,
SingleCallValidator(
ChainRequest("health", ListParams()),
) { _ -> OK },
)
}

override fun lowerBoundService(chain: Chain, upstream: Upstream) =
CosmosLowerBoundService(chain, upstream)

override fun upstreamSettingsDetector(chain: Chain, upstream: Upstream) =
CosmosUpstreamSettingsDetector(upstream)
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosBlockResult(
@JsonProperty("block_id") var blockId: CosmosBlockId,
@JsonProperty("block") var block: CosmosBlockData,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosBlockId(
@JsonProperty("hash") var hash: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosHeader(
@JsonProperty("last_block_id") var lastBlockId: CosmosBlockId,
@JsonProperty("height") var height: String,
@JsonProperty("time") var time: Instant,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosStatus(
@JsonProperty("node_info") var nodeInfo: CosmosNodeInfo,
@JsonProperty("sync_info") var syncInfo: CosmosSyncInfo,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosNodeInfo(
@JsonProperty("version") var version: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosSyncInfo(
@JsonProperty("earliest_block_height") var earliestBlockHeight: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosBlockEvent(
@JsonProperty("data") var data: CosmosBlockEventData,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class CosmosBlockEventData(
@JsonProperty("value") var value: CosmosBlockData,
)

data class CosmosBlockData(
@JsonProperty("header") var header: CosmosHeader,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.emeraldpay.dshackle.upstream.cosmos

import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundDetector
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType.STATE
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import reactor.core.publisher.Flux
import reactor.kotlin.core.publisher.toFlux

class CosmosLowerBoundService(
chain: Chain,
private val upstream: Upstream,
) : LowerBoundService(chain, upstream) {
override fun detectors(): List<LowerBoundDetector> {
return listOf(CosmosLowerBoundStateDetector(upstream))
}
}

class CosmosLowerBoundStateDetector(
private val upstream: Upstream,
) : LowerBoundDetector() {

override fun period(): Long {
return 3
}

override fun internalDetectLowerBound(): Flux<LowerBoundData> {
return upstream.getIngressReader().read(ChainRequest("status", ListParams())).map {
val resp = Global.objectMapper.readValue(it.getResult(), CosmosStatus::class.java)
LowerBoundData(resp.syncInfo.earliestBlockHeight.toLong(), STATE)
}.toFlux()
}

override fun types(): Set<LowerBoundType> {
return setOf(STATE)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.emeraldpay.dshackle.upstream.cosmos

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.ListParams
import reactor.core.publisher.Flux

class CosmosUpstreamSettingsDetector(
upstream: Upstream,
) : BasicUpstreamSettingsDetector(upstream) {
override fun detectLabels(): Flux<Pair<String, String>> {
return Flux.merge(
detectNodeType(),
)
}

override fun clientVersionRequest(): ChainRequest {
return ChainRequest("status", ListParams())
}

override fun parseClientVersion(data: ByteArray): String {
return Global.objectMapper.readValue<CosmosStatus>(data).nodeInfo.version
}

override fun nodeTypeRequest(): NodeTypeRequest = NodeTypeRequest(clientVersionRequest())

override fun clientType(node: JsonNode): String? = null

override fun clientVersion(node: JsonNode): String? =
node.get("node_info")?.get("version")?.asText() ?: UNKNOWN_CLIENT_VERSION
}
Loading
Loading