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

feat(sdk): asset lock quorum and core locked height verification #2030

Open
wants to merge 18 commits into
base: v1.7-dev
Choose a base branch
from
Open
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
57 changes: 31 additions & 26 deletions packages/dapi-grpc/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,21 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
// Derive features for versioned messages
//
// "GetConsensusParamsRequest" is excluded as this message does not support proofs
const VERSIONED_REQUESTS: [&str; 30] = [
//
// Please place in alphabetical order to make maintenance easier.
const VERSIONED_REQUESTS: [&str; 31] = [
"GetContestedResourcesRequest",
"GetContestedResourceVoteStateRequest",
"GetContestedResourceVotersForIdentityRequest",
"GetContestedResourceIdentityVotesRequest",
"GetDataContractHistoryRequest",
"GetDataContractRequest",
"GetDataContractsRequest",
"GetDocumentsRequest",
"GetEpochsInfoRequest",
"GetEvonodesProposedEpochBlocksByIdsRequest",
"GetEvonodesProposedEpochBlocksByRangeRequest",
"GetIdentitiesContractKeysRequest",
"GetIdentitiesByPublicKeyHashesRequest",
"GetIdentitiesRequest",
"GetIdentitiesBalancesRequest",
Expand All @@ -62,59 +72,54 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
"GetIdentityByPublicKeyHashRequest",
"GetIdentityKeysRequest",
"GetIdentityRequest",
"GetPathElementsRequest",
"GetPrefundedSpecializedBalanceRequest",
"GetProofsRequest",
"WaitForStateTransitionResultRequest",
"GetProtocolVersionUpgradeStateRequest",
"GetProtocolVersionUpgradeVoteStatusRequest",
"GetPathElementsRequest",
"GetIdentitiesContractKeysRequest",
"GetPrefundedSpecializedBalanceRequest",
"GetContestedResourcesRequest",
"GetContestedResourceVoteStateRequest",
"GetContestedResourceVotersForIdentityRequest",
"GetContestedResourceIdentityVotesRequest",
"GetVotePollsByEndDateRequest",
"GetTotalCreditsInPlatformRequest",
"GetEvonodesProposedEpochBlocksByIdsRequest",
"GetEvonodesProposedEpochBlocksByRangeRequest",
"GetStatusRequest",
"GetTotalCreditsInPlatformRequest",
"GetVotePollsByEndDateRequest",
"WaitForStateTransitionResultRequest",
];

// The following responses are excluded as they don't support proofs:
// - "GetConsensusParamsResponse"
// - "GetStatusResponse"
//
// "GetEvonodesProposedEpochBlocksResponse" is used for 2 Requests
// "GetEvonodesProposedEpochBlocksResponse" is used for 2 Requests.
//
// Please place in alphabetical order to make maintenance easier.
const VERSIONED_RESPONSES: [&str; 29] = [
"GetContestedResourcesResponse",
"GetContestedResourceVoteStateResponse",
"GetContestedResourceVotersForIdentityResponse",
"GetContestedResourceIdentityVotesResponse",
"GetDataContractHistoryResponse",
"GetDataContractResponse",
"GetDataContractsResponse",
"GetDocumentsResponse",
"GetEpochsInfoResponse",
"GetEvonodesProposedEpochBlocksResponse",
"GetIdentitiesByPublicKeyHashesResponse",
"GetIdentitiesResponse",
"GetIdentitiesBalancesResponse",
"GetIdentityBalanceAndRevisionResponse",
"GetIdentityBalanceResponse",
"GetIdentityNonceResponse",
"GetIdentitiesContractKeysResponse",
"GetIdentityContractNonceResponse",
"GetIdentityNonceResponse",
"GetIdentityByPublicKeyHashResponse",
"GetIdentityKeysResponse",
"GetIdentityResponse",
"GetPathElementsResponse",
"GetPrefundedSpecializedBalanceResponse",
"GetProofsResponse",
"WaitForStateTransitionResultResponse",
"GetEpochsInfoResponse",
"GetProtocolVersionUpgradeStateResponse",
"GetProtocolVersionUpgradeVoteStatusResponse",
"GetPathElementsResponse",
"GetIdentitiesContractKeysResponse",
"GetPrefundedSpecializedBalanceResponse",
"GetContestedResourcesResponse",
"GetContestedResourceVoteStateResponse",
"GetContestedResourceVotersForIdentityResponse",
"GetContestedResourceIdentityVotesResponse",
"GetVotePollsByEndDateResponse",
"GetTotalCreditsInPlatformResponse",
"GetEvonodesProposedEpochBlocksResponse",
"GetVotePollsByEndDateResponse",
"WaitForStateTransitionResultResponse",
];

check_unique(&VERSIONED_REQUESTS).expect("VERSIONED_REQUESTS");
Expand Down
17 changes: 17 additions & 0 deletions packages/rs-dapi-client/src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::transport::TransportRequest;
use crate::{Address, CanRetry, DapiClientError, RequestSettings};
use dapi_grpc::mock::Mockable;
use dapi_grpc::platform::VersionedGrpcResponse;
use dapi_grpc::tonic::async_trait;
use std::fmt::Debug;

Expand Down Expand Up @@ -121,6 +122,22 @@ where
}
}

impl<T: VersionedGrpcResponse> VersionedGrpcResponse for ExecutionResponse<T> {
type Error = T::Error;

fn metadata(&self) -> Result<&dapi_grpc::platform::v0::ResponseMetadata, Self::Error> {
self.inner.metadata()
}

fn proof(&self) -> Result<&dapi_grpc::platform::v0::Proof, Self::Error> {
self.inner.proof()
}

fn proof_owned(self) -> Result<dapi_grpc::platform::v0::Proof, Self::Error> {
self.inner.proof_owned()
}
}

/// Result of request execution
pub type ExecutionResult<R, E> = Result<ExecutionResponse<R>, ExecutionError<E>>;

Expand Down
2 changes: 1 addition & 1 deletion packages/rs-dpp/src/core_types/validator_set/v0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ mod tests {
let node_ip = "192.168.1.1".to_string();
let node_id = PubkeyHash::from_slice(&[4; 20]).unwrap();
let validator = ValidatorV0 {
pro_tx_hash: pro_tx_hash.clone(),
pro_tx_hash,
public_key,
node_ip,
node_id,
Expand Down
28 changes: 27 additions & 1 deletion packages/rs-sdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ pub enum Error {
/// Epoch not found; we must have at least one epoch
#[error("No epoch found on Platform; it should never happen")]
EpochNotFound,
/// Quorum not found; try again later
#[error(
"Quorum {quorum_hash_hex} of type {quorum_type} at height {core_chain_locked_height}: {e}"
)]
QuorumNotFound {
quorum_hash_hex: String,
quorum_type: u32,
core_chain_locked_height: u32,
e: ContextProviderError,
},

/// Asset lock not found; try again later.
///
/// ## Parameters
///
/// - 0 - core locked height in asset lock
/// - 1 - current core locked height on the platform
#[error("Asset lock for core locked height {0} not available yet, max avaiable locked core height is {1}; try again later")]
CoreLockedHeightNotYetAvailable(u32, u32),

/// SDK operation timeout reached error
#[error("SDK operation timeout {} secs reached: {1}", .0.as_secs())]
TimeoutReached(Duration, String),
Expand Down Expand Up @@ -127,7 +147,13 @@ where

impl CanRetry for Error {
fn can_retry(&self) -> bool {
matches!(self, Error::StaleNode(..) | Error::TimeoutReached(_, _))
matches!(
self,
Error::StaleNode(..)
| Error::TimeoutReached(_, _)
| Error::CoreLockedHeightNotYetAvailable(_, _)
| Error::QuorumNotFound { .. }
)
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/rs-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub mod core;
pub mod error;
mod internal_cache;
pub mod mock;
pub mod networks;
pub mod platform;
pub mod sdk;

Expand Down
91 changes: 91 additions & 0 deletions packages/rs-sdk/src/networks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Configuration of dash networks (devnet, testnet, mainnet, etc.).
//!
//! See also:
//! * https://github.com/dashpay/dash/blob/develop/src/chainparams.cpp

/*
Mainnet:
consensus.llmqTypeChainLocks = Consensus::LLMQType::LLMQ_400_60;
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_100_67;
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_400_85;

Testnet:
consensus.llmqTypeChainLocks = Consensus::LLMQType::LLMQ_50_60;
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_25_67;
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_50_60;

Devnet:
consensus.llmqTypeChainLocks = Consensus::LLMQType::LLMQ_DEVNET;
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_DEVNET_DIP0024;
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_DEVNET_PLATFORM;
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_DEVNET;

*/

use dashcore_rpc::json::QuorumType;

/// Dash network types.
#[derive(Eq, PartialEq, Clone, Debug)]
pub enum NetworkType {
/// Mock implementation; in practice, feaults to Devnet config for Mock mode. Errors when used in non-mock mode.
Mock,
/// Mainnet network, used for production.
Mainnet,
/// Testnet network, used for testing and development.
Testnet,
/// Devnet network, used local for development.
Devnet,
/// Custom network configuration.
Custom(QuorumParams),
}

impl NetworkType {
pub fn instant_lock_quorum_type(&self) -> QuorumType {
self.to_quorum_params().instant_lock_quorum_type
}

pub(crate) fn to_quorum_params(&self) -> QuorumParams {
match self {
NetworkType::Mainnet => QuorumParams::new_mainnet(),
NetworkType::Testnet => QuorumParams::new_testnet(),
NetworkType::Devnet => QuorumParams::new_devnet(),
NetworkType::Custom(config) => config.clone(),
NetworkType::Mock => QuorumParams::new_mock(),
}
}
}

/// Configuration of Dash Core Quorums.
///
/// In most cases, you should use the [`new_mainnet`] or [`new_testnet`] functions to create a new instance.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QuorumParams {
pub instant_lock_quorum_type: QuorumType,
}

impl QuorumParams {
pub fn new_mainnet() -> Self {
QuorumParams {
instant_lock_quorum_type: QuorumType::Llmq400_60,
}
}

pub fn new_testnet() -> Self {
QuorumParams {
instant_lock_quorum_type: QuorumType::Llmq50_60,
}
}

pub fn new_devnet() -> Self {
QuorumParams {
// FIXME: local devnet uses regtest
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs discussion how to handle that.

instant_lock_quorum_type: QuorumType::LlmqTest,
}
}

pub fn new_mock() -> Self {
Self::new_devnet()
}
}
1 change: 1 addition & 0 deletions packages/rs-sdk/src/platform/transition.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! State transitions used to put changed objects to the Dash Platform.
pub mod asset_lock;
pub mod broadcast;
pub(crate) mod broadcast_identity;
pub mod broadcast_request;
Expand Down
82 changes: 82 additions & 0 deletions packages/rs-sdk/src/platform/transition/asset_lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! [AssetLockProof] utilities

use crate::{Error, Sdk};
use dapi_grpc::platform::v0::get_epochs_info_request::{self, GetEpochsInfoRequestV0};
use dapi_grpc::platform::v0::GetEpochsInfoRequest;
use dapi_grpc::platform::VersionedGrpcResponse;
use dpp::dashcore::hashes::Hash;
use dpp::prelude::AssetLockProof;
use drive_proof_verifier::error::ContextProviderError;
use drive_proof_verifier::ContextProvider;
use rs_dapi_client::{DapiRequestExecutor, RequestSettings};
#[async_trait::async_trait]
pub trait AssetLockProofVerifier {
/// Verifies the asset lock proof against the platform.
///
/// This function will return an error if Dash Platform cannot use the provided asset lock proof.
///
/// # Errors
///
/// - [Error::CoreLockedHeightNotYetAvailable] if the core locked height in the proof is higher than the
/// current core locked height on the platform. Try again later.
/// - [Error::QuorumNotFound] if the quorum public key is not yet available on the platform, what implies that
/// the quorum is not (yet) available. Try again later.
/// - other errors when something goes wrong.
async fn verify(&self, sdk: &Sdk) -> Result<(), Error>;
}

#[async_trait::async_trait]
impl AssetLockProofVerifier for AssetLockProof {
async fn verify(&self, sdk: &Sdk) -> Result<(), Error> {
let context_provider = sdk
.context_provider()
.ok_or(Error::Config("Context Provider not configured".to_string()))?;

// Retrieve current core chain lock info from the platform
// TODO: implement some caching mechanism to avoid fetching the same data multiple times
let request = GetEpochsInfoRequest {
version: Some(get_epochs_info_request::Version::V0(
GetEpochsInfoRequestV0 {
ascending: false,
count: 1,
prove: true,
start_epoch: None,
},
)),
};
let response = sdk.execute(request, RequestSettings::default()).await?;
let platform_core_chain_locked_height = response.metadata()?.core_chain_locked_height;

match self {
AssetLockProof::Chain(asset_lock) => {
if asset_lock.core_chain_locked_height > platform_core_chain_locked_height {
Err(Error::CoreLockedHeightNotYetAvailable(
asset_lock.core_chain_locked_height,
platform_core_chain_locked_height,
))
} else {
Ok(())
}
}
AssetLockProof::Instant(v) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not gonna check if we can verify signature in Drive, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have any hint how to do that client-side, I would be happy to include this here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@QuantumExplorer can you give me some formula to add it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what we discussed, we have no better solution than what's already implemented.

let quorum_hash = v.instant_lock().cyclehash.to_raw_hash().to_byte_array();
let quorum_type = sdk.quorum_params().instant_lock_quorum_type;
// Try to fetch the quorum public key; if it fails, we assume platform does not have this quorum yet
match context_provider.get_quorum_public_key(
quorum_type as u32,
quorum_hash,
platform_core_chain_locked_height,
) {
Err(ContextProviderError::InvalidQuorum(s)) => Err(Error::QuorumNotFound {
e: ContextProviderError::InvalidQuorum(s),
quorum_hash_hex: hex::encode(quorum_hash),
quorum_type: quorum_type as u32,
core_chain_locked_height: platform_core_chain_locked_height,
}),
Err(e) => Err(e.into()),
Ok(_) => Ok(()),
}
}
}
}
}
1 change: 1 addition & 0 deletions packages/rs-sdk/src/platform/transition/put_identity.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::platform::block_info_from_metadata::block_info_from_metadata;
use crate::platform::transition::broadcast_identity::BroadcastRequestForNewIdentity;
use crate::{Error, Sdk};

Expand Down
2 changes: 1 addition & 1 deletion packages/rs-sdk/src/platform/types/epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
Error, Sdk,
};

/// Epoch type used in the SDK.
/// Epoch information
pub type Epoch = ExtendedEpochInfo;

#[async_trait]
Expand Down
Loading
Loading