Skip to content

Commit

Permalink
add cosmos rpc support
Browse files Browse the repository at this point in the history
  • Loading branch information
a10zn8 committed May 31, 2024
1 parent a2ae05e commit 4aff359
Show file tree
Hide file tree
Showing 18 changed files with 350 additions and 16 deletions.
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

0 comments on commit 4aff359

Please sign in to comment.