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 19 commits into
base: v1.8-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
5 changes: 3 additions & 2 deletions packages/dapi-grpc/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ 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; 26] = [
const VERSIONED_REQUESTS: [&str; 27] = [
"GetDataContractHistoryRequest",
"GetDataContractRequest",
"GetDataContractsRequest",
"GetDocumentsRequest",
"GetEpochsInfoRequest",
"GetIdentitiesByPublicKeyHashesRequest",
"GetIdentitiesRequest",
"GetIdentityNonceRequest",
Expand Down Expand Up @@ -82,6 +83,7 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
"GetDataContractResponse",
"GetDataContractsResponse",
"GetDocumentsResponse",
"GetEpochsInfoResponse",
"GetIdentitiesByPublicKeyHashesResponse",
"GetIdentitiesResponse",
"GetIdentityBalanceAndRevisionResponse",
Expand All @@ -93,7 +95,6 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
"GetIdentityResponse",
"GetProofsResponse",
"WaitForStateTransitionResultResponse",
"GetEpochsInfoResponse",
"GetProtocolVersionUpgradeStateResponse",
"GetProtocolVersionUpgradeVoteStatusResponse",
"GetPathElementsResponse",
Expand Down
14 changes: 7 additions & 7 deletions packages/rs-dapi-client/src/dapi_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ pub enum DapiClientError<TE: Mockable> {
}

impl<TE: CanRetry + Mockable> CanRetry for DapiClientError<TE> {
fn is_node_failure(&self) -> bool {
fn can_retry(&self) -> bool {
use DapiClientError::*;
match self {
NoAvailableAddresses => false,
Transport(transport_error, _) => transport_error.is_node_failure(),
AddressList(_) => false,
NoAvailableAddresses => true,
Transport(transport_error, _) => transport_error.can_retry(),
AddressList(_) => true,
#[cfg(feature = "mocks")]
Mock(_) => false,
}
Expand Down Expand Up @@ -222,7 +222,7 @@ impl DapiRequestExecutor for DapiClient {
tracing::trace!(?response, "received {} response", response_name);
}
Err(error) => {
if error.is_node_failure() {
if !error.can_retry() {
if applied_settings.ban_failed_address {
let mut address_list = self
.address_list
Expand Down Expand Up @@ -253,12 +253,12 @@ impl DapiRequestExecutor for DapiClient {
duration.as_secs_f32()
)
})
.when(|e| e.is_node_failure())
.when(|e| !e.can_retry())
.instrument(tracing::info_span!("request routine"))
.await;

if let Err(error) = &result {
if error.is_node_failure() {
if !error.can_retry() {
tracing::error!(?error, "request failed");
}
}
Expand Down
10 changes: 9 additions & 1 deletion packages/rs-dapi-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ impl<T: transport::TransportRequest + Send> DapiRequest for T {
/// Allows to flag the transport error variant how tolerant we are of it and whether we can
/// try to do a request again.
pub trait CanRetry {
/// Returns true if the operation can be retried safely, false means it's unspecified
fn can_retry(&self) -> bool;

/// Get boolean flag that indicates if the error is retryable.
fn is_node_failure(&self) -> bool;
///
/// Depreacted in favor of [CanRetry::can_retry].
#[deprecated = "Use !can_retry() instead"]
fn is_node_failure(&self) -> bool {
!self.can_retry()
}
}
4 changes: 2 additions & 2 deletions packages/rs-dapi-client/src/transport/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ impl TransportClient for CoreGrpcClient {
}

impl CanRetry for dapi_grpc::tonic::Status {
fn is_node_failure(&self) -> bool {
fn can_retry(&self) -> bool {
let code = self.code();

use dapi_grpc::tonic::Code::*;
matches!(
!matches!(
code,
Ok | DataLoss
| Cancelled
Expand Down
32 changes: 31 additions & 1 deletion packages/rs-sdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::time::Duration;
use dapi_grpc::mock::Mockable;
use dpp::version::PlatformVersionError;
use dpp::ProtocolError;
use rs_dapi_client::DapiClientError;
use rs_dapi_client::{CanRetry, DapiClientError};

pub use drive_proof_verifier::error::ContextProviderError;

Expand Down Expand Up @@ -52,6 +52,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 @@ -80,3 +100,13 @@ impl From<PlatformVersionError> for Error {
Self::Protocol(value.into())
}
}
impl CanRetry for Error {
/// Returns true if the operation can be retried, false means it's unspecified
/// False means
fn can_retry(&self) -> bool {
matches!(
self,
Error::CoreLockedHeightNotYetAvailable(_, _) | Error::QuorumNotFound { .. }
)
}
}
1 change: 1 addition & 0 deletions packages/rs-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ mod core_client;
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,
Copy link
Member

Choose a reason for hiding this comment

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

Should receive quorum params

/// Custom network configuration.
Custom(QuorumParams),
Copy link
Member

Choose a reason for hiding this comment

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

Local

}

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 {
Copy link
Member

Choose a reason for hiding this comment

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

We have quorum params in Drive and dashcore library. I think the correct place for them is dashcore lib

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 {
Copy link
Member

Choose a reason for hiding this comment

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

We don't need this for instant lock verification

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 {
Copy link
Member

Choose a reason for hiding this comment

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

It must be lower or equal

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(()),
Copy link
Member

Choose a reason for hiding this comment

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

You need to verify signature

}
}
}
}
}
8 changes: 3 additions & 5 deletions packages/rs-sdk/src/platform/transition/put_identity.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
use crate::platform::block_info_from_metadata::block_info_from_metadata;
use crate::platform::transition::broadcast_identity::BroadcastRequestForNewIdentity;
use crate::platform::transition::broadcast_request::BroadcastRequestForStateTransition;
use crate::platform::Fetch;
use crate::{Error, Sdk};

use dapi_grpc::platform::VersionedGrpcResponse;
use dapi_grpc::tonic::Code;
use dpp::dashcore::PrivateKey;
use dpp::identity::signer::Signer;
use dpp::prelude::{AssetLockProof, Identity};
use drive_proof_verifier::error::ContextProviderError;
use drive_proof_verifier::DataContractProvider;

use crate::platform::block_info_from_metadata::block_info_from_metadata;
use dpp::state_transition::proof_result::StateTransitionProofResult;
use drive::drive::Drive;
use drive_proof_verifier::error::ContextProviderError;
use drive_proof_verifier::DataContractProvider;
use rs_dapi_client::{DapiClientError, DapiRequest, RequestSettings};

#[async_trait::async_trait]
Expand Down
3 changes: 3 additions & 0 deletions packages/rs-sdk/src/platform/types/epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use crate::{
Error, Sdk,
};

/// Epoch information
pub type Epoch = ExtendedEpochInfo;

#[async_trait]
impl FetchCurrent for ExtendedEpochInfo {
async fn fetch_current(sdk: &Sdk) -> Result<Self, Error> {
Expand Down
Loading
Loading