diff --git a/packages/dapi-grpc/build.rs b/packages/dapi-grpc/build.rs index 4c8f1d1aa35..e80dcec6d85 100644 --- a/packages/dapi-grpc/build.rs +++ b/packages/dapi-grpc/build.rs @@ -63,41 +63,44 @@ 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; 34] = [ + // + // Sort alphabetically to simplify maintenance. + const VERSIONED_REQUESTS: [&str; 35] = [ + "GetContestedResourceIdentityVotesRequest", + "GetContestedResourcesRequest", + "GetContestedResourceVoteStateRequest", + "GetContestedResourceVotersForIdentityRequest", "GetDataContractHistoryRequest", "GetDataContractRequest", "GetDataContractsRequest", "GetDocumentsRequest", + "GetEpochsInfoRequest", + "GetEvonodesProposedEpochBlocksByIdsRequest", + "GetEvonodesProposedEpochBlocksByRangeRequest", + "GetIdentitiesBalancesRequest", "GetIdentitiesByPublicKeyHashesRequest", + "GetIdentitiesContractKeysRequest", "GetIdentitiesRequest", - "GetIdentitiesBalancesRequest", - "GetIdentityNonceRequest", - "GetIdentityContractNonceRequest", + "GetIdentitiesTokenBalancesRequest", + "GetIdentitiesTokenInfosRequest", "GetIdentityBalanceAndRevisionRequest", "GetIdentityBalanceRequest", "GetIdentityByPublicKeyHashRequest", + "GetIdentityContractNonceRequest", "GetIdentityKeysRequest", + "GetIdentityNonceRequest", "GetIdentityRequest", + "GetIdentityTokenBalancesRequest", + "GetIdentityTokenInfosRequest", + "GetPathElementsRequest", + "GetPrefundedSpecializedBalanceRequest", "GetProofsRequest", - "WaitForStateTransitionResultRequest", "GetProtocolVersionUpgradeStateRequest", "GetProtocolVersionUpgradeVoteStatusRequest", - "GetPathElementsRequest", - "GetIdentitiesContractKeysRequest", - "GetPrefundedSpecializedBalanceRequest", - "GetContestedResourcesRequest", - "GetContestedResourceVoteStateRequest", - "GetContestedResourceVotersForIdentityRequest", - "GetContestedResourceIdentityVotesRequest", - "GetVotePollsByEndDateRequest", - "GetTotalCreditsInPlatformRequest", - "GetEvonodesProposedEpochBlocksByIdsRequest", - "GetEvonodesProposedEpochBlocksByRangeRequest", "GetStatusRequest", - "GetIdentityTokenBalancesRequest", - "GetIdentitiesTokenBalancesRequest", - "GetIdentityTokenInfosRequest", - "GetIdentitiesTokenInfosRequest", + "GetTotalCreditsInPlatformRequest", + "GetVotePollsByEndDateRequest", + "WaitForStateTransitionResultRequest", ]; // The following responses are excluded as they don't support proofs: @@ -105,40 +108,42 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig { // - "GetStatusResponse" // // "GetEvonodesProposedEpochBlocksResponse" is used for 2 Requests + // + // Sort alphabetically to simplify maintenance. const VERSIONED_RESPONSES: [&str; 33] = [ + "GetContestedResourceIdentityVotesResponse", + "GetContestedResourcesResponse", + "GetContestedResourceVoteStateResponse", + "GetContestedResourceVotersForIdentityResponse", "GetDataContractHistoryResponse", "GetDataContractResponse", "GetDataContractsResponse", "GetDocumentsResponse", - "GetIdentitiesByPublicKeyHashesResponse", - "GetIdentitiesResponse", - "GetIdentitiesBalancesResponse", + "GetEpochsInfoResponse", + "GetEvonodesProposedEpochBlocksResponse", "GetIdentityBalanceAndRevisionResponse", "GetIdentityBalanceResponse", - "GetIdentityNonceResponse", - "GetIdentityContractNonceResponse", "GetIdentityByPublicKeyHashResponse", + "GetIdentityContractNonceResponse", "GetIdentityKeysResponse", + "GetIdentityNonceResponse", "GetIdentityResponse", + "GetIdentityTokenBalancesResponse", + "GetIdentityTokenInfosResponse", + "GetIdentitiesBalancesResponse", + "GetIdentitiesByPublicKeyHashesResponse", + "GetIdentitiesContractKeysResponse", + "GetIdentitiesResponse", + "GetIdentitiesTokenBalancesResponse", + "GetIdentitiesTokenInfosResponse", + "GetPathElementsResponse", + "GetPrefundedSpecializedBalanceResponse", "GetProofsResponse", - "WaitForStateTransitionResultResponse", - "GetEpochsInfoResponse", "GetProtocolVersionUpgradeStateResponse", "GetProtocolVersionUpgradeVoteStatusResponse", - "GetPathElementsResponse", - "GetIdentitiesContractKeysResponse", - "GetPrefundedSpecializedBalanceResponse", - "GetContestedResourcesResponse", - "GetContestedResourceVoteStateResponse", - "GetContestedResourceVotersForIdentityResponse", - "GetContestedResourceIdentityVotesResponse", - "GetVotePollsByEndDateResponse", "GetTotalCreditsInPlatformResponse", - "GetEvonodesProposedEpochBlocksResponse", - "GetIdentityTokenBalancesResponse", - "GetIdentitiesTokenBalancesResponse", - "GetIdentityTokenInfosResponse", - "GetIdentitiesTokenInfosResponse", + "GetVotePollsByEndDateResponse", + "WaitForStateTransitionResultResponse", ]; check_unique(&VERSIONED_REQUESTS).expect("VERSIONED_REQUESTS"); diff --git a/packages/rs-dapi-client/src/executor.rs b/packages/rs-dapi-client/src/executor.rs index c87b2f9336b..56c77e465dd 100644 --- a/packages/rs-dapi-client/src/executor.rs +++ b/packages/rs-dapi-client/src/executor.rs @@ -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; @@ -127,6 +128,22 @@ where } } +impl VersionedGrpcResponse for ExecutionResponse { + 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 { + self.inner.proof_owned() + } +} + /// Result of request execution pub type ExecutionResult = Result, ExecutionError>; diff --git a/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs b/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs index 4789d53dd62..bb190f5c194 100644 --- a/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs +++ b/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs @@ -311,7 +311,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, diff --git a/packages/rs-sdk/src/error.rs b/packages/rs-sdk/src/error.rs index 276595958e4..4a59122bdc9 100644 --- a/packages/rs-sdk/src/error.rs +++ b/packages/rs-sdk/src/error.rs @@ -55,6 +55,34 @@ 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), + + /// Provided asset lock is invalid + /// + /// ## Parameters + /// + /// - 0 - detailed error message + #[error("Invalid asset lock: {0}")] + InvalidAssetLock(String), + /// SDK operation timeout reached error #[error("SDK operation timeout {} secs reached: {1}", .0.as_secs())] TimeoutReached(Duration, String), @@ -100,7 +128,7 @@ impl TryFrom for StateTransitionBroadcastErr type Error = Error; fn try_from(value: StateTransitionBroadcastErrorProto) -> Result { - let cause = if value.data.len() > 0 { + let cause = if !value.data.is_empty() { let consensus_error = ConsensusError::deserialize_from_bytes(&value.data).map_err(|e| { tracing::debug!("Failed to deserialize consensus error: {}", e); @@ -178,7 +206,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 { .. } + ) } } diff --git a/packages/rs-sdk/src/lib.rs b/packages/rs-sdk/src/lib.rs index fe2c51ab065..1bb884c9764 100644 --- a/packages/rs-sdk/src/lib.rs +++ b/packages/rs-sdk/src/lib.rs @@ -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; diff --git a/packages/rs-sdk/src/networks.rs b/packages/rs-sdk/src/networks.rs new file mode 100644 index 00000000000..b1d815b7aa6 --- /dev/null +++ b/packages/rs-sdk/src/networks.rs @@ -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 + instant_lock_quorum_type: QuorumType::LlmqTest, + } + } + + pub fn new_mock() -> Self { + Self::new_devnet() + } +} diff --git a/packages/rs-sdk/src/platform/transition.rs b/packages/rs-sdk/src/platform/transition.rs index 234537179de..854043f0eaf 100644 --- a/packages/rs-sdk/src/platform/transition.rs +++ b/packages/rs-sdk/src/platform/transition.rs @@ -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; diff --git a/packages/rs-sdk/src/platform/transition/asset_lock.rs b/packages/rs-sdk/src/platform/transition/asset_lock.rs new file mode 100644 index 00000000000..06ce7fa2dd7 --- /dev/null +++ b/packages/rs-sdk/src/platform/transition/asset_lock.rs @@ -0,0 +1,245 @@ +//! [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 dashcore_rpc::json::QuorumType; +use dpp::bls_signatures::{Bls12381G2Impl, BlsError, Pairing, PublicKey, Signature}; +use dpp::dashcore::hashes::{sha256d, Hash, HashEngine}; +use dpp::dashcore::InstantLock; +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. + /// - [Error::InvalidSignature] if the signature in the proof is invalid. + /// - 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(instant_asset_lock_proof) => { + instant_asset_lock_proof.validate_structure(sdk.version())?; + + let quorum_hash = instant_asset_lock_proof + .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 + let quorum_pubkey = 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(key) => Ok(key), + }?; + + verify_instant_lock_signature( + instant_asset_lock_proof.instant_lock(), + &quorum_type, + &quorum_hash, + &quorum_pubkey, + ) + .map_err(|e| { + Error::InvalidAssetLock(format!( + "error during instant asset lock verification: {}", + e + )) + }) + .and_then(|correct| { + if correct { + Ok(()) + } else { + Err(Error::InvalidAssetLock( + "invalid asset lock signature".to_string(), + )) + } + }) + } + } + } +} +/// Verify instant lock signature. +/// +/// Retrurns Ok(true) when signature is valid, Ok(false) when signature is invalid, or an error when something goes wrong. +/// +/// Note: This is a generalisation [verify_recent_instant_lock_signature_locally_v0] from drive-abci. +/// +/// TODO: Discuss moving to some other place to reuse in drive-abci and here +fn verify_instant_lock_signature( + instant_lock: &InstantLock, + quorum_type: &QuorumType, + quorum_hash: &[u8], + quorum_public_key: &[u8; 48], +) -> Result { + // First verify signature format + let signature: Signature = + match ::Signature::from_compressed( + instant_lock.signature.as_bytes(), + ) + .into_option() + { + Some(signature) => Signature::Basic(signature), + None => { + tracing::warn!( + instant_lock = ?instant_lock, + "Invalid instant lock {} signature format", + instant_lock.txid, + ); + + return Ok(false); + } + }; + + let threshold_public_key = + PublicKey::try_from(quorum_public_key.as_slice()).map_err(|e| Error::Protocol(e.into()))?; + + let request_id = instant_lock.request_id().map_err(|e| { + Error::Protocol(dpp::ProtocolError::EncodingError(format!( + "cannot create instant asset lock request id: {}", + e + ))) + })?; + + // The signature must verify against the quorum public key and SHA256(llmqType, quorumHash, SHA256(height), txId). + // llmqType and quorumHash must be taken from the quorum selected in 1. + let mut engine = sha256d::Hash::engine(); + + let mut reversed_quorum_hash = quorum_hash.to_vec(); + reversed_quorum_hash.reverse(); + + engine.input(&[*quorum_type as u8]); + engine.input(reversed_quorum_hash.as_slice()); + engine.input(request_id.as_byte_array()); + engine.input(instant_lock.txid.as_byte_array()); + + let message_digest = sha256d::Hash::from_engine(engine); + + match signature.verify( + &threshold_public_key, + message_digest.as_byte_array().as_slice(), + ) { + Ok(()) => Ok(true), + Err(BlsError::InvalidSignature) => Ok(false), + Err(e) => Err(Error::Protocol(e.into())), + } +} + +#[cfg(test)] +mod tests { + + use dashcore_rpc::json::QuorumType; + use dpp::dashcore::{consensus::deserialize, InstantLock}; + + use crate::platform::transition::asset_lock::verify_instant_lock_signature; + + /// Test signature verification on an instant asset lock. + #[test] + fn test_verify_instant_lock_signature() { + const INSTANT_SEND_LOCK_QUORUM_TYPE: QuorumType = QuorumType::Llmq60_75; + + struct TestCase { + instant_lock_hex: &'static str, + quorum_hash: &'static str, + quorum_public_key: &'static str, + correct_signature: bool, + } + + // test vector generated with: + // dash-cli getislocks '["e7ca636c01d65d4e445a802cf69e261676ba5e2a7e0ae57949e2ce5e40bf99ce"]' + let test_cases = vec![ + TestCase{ + instant_lock_hex: "010c828b35aa1ba4ec8ff7b34335f8d4179080f2ed3a0d63f349b8adc868c73ff201010000007a46cb5ef53943cec615f9054acc460418129e4f13ee33cc1f5c39707c11401b020000005d63eaab8d9d29e48e041b9f5b43e3e41b5dac21cd998e703b631198b77b731c0c0000002f7f367e3e9461b8c6c2d919f276a0e344e673dfc84da21b8ff9606fa64f4429050000009ca17ff3474487cfc718e871696bd1d4d7669efced04aa5cdb4cc69f906da639000000009ca17ff3474487cfc718e871696bd1d4d7669efced04aa5cdb4cc69f906da639010000002e35829452aae4996c47ead73b20183bad78e83cb29185f4be9ca60b80e8c06208000000e9ec07776bc2337daa6ee7549b9ba38d8782f0581a21555bc9dde8156c713d6c080000009ba8c0f9ddc2531b576244930e75e66752e8a3dd70d3262ee1b379f58d19ba7d06000000efb7d48484544cfa06585494b427862f098708707ffa1610b4ed33a724bfe4d002000000efb7d48484544cfa06585494b427862f098708707ffa1610b4ed33a724bfe4d00300000042f2373701b15830d8fba26001b4480963db6cf238936eb8d6705ec3fe4cc9f801000000ce99bf405ecee24979e50a7e2a5eba7616269ef62c805a444e5dd6016c63cae72ed3582b963bbda2fdec219ff84299c525dfadcf4b69218a1400000000000000b0f624683b04c01ad58bcf6233f6e492faf0e7f9adf0609dab04a97796aabf13175262c74056690610c075d8cce2cc9f12549b8d5ab3484be19b9c3ebcc9c29dd8d8762bb6e940cc79acfc2940f709a3fa5ff0be11c1304c219931ce58dbac95", + quorum_hash: "000000000000000e1fd2afb1f179b1f5c2f50a3614da93c0e40c4a1ddd921200", + quorum_public_key: "a1ce5102d30de044adccb2a64d1120db355d222a1478b5e2a3f4bf7f90895ac01eccde2ff8cec27c9a886bdfa0b83b1a", + correct_signature: true, + }, + TestCase{ + instant_lock_hex: "010c828b35aa1ba4ec8ff7b34335f8d4179080f2ed3a0d63f349b8adc868c73ff201010000007a46cb5ef53943cec615f9054acc460418129e4f13ee33cc1f5c39707c11401b020000005d63eaab8d9d29e48e041b9f5b43e3e41b5dac21cd998e703b631198b77b731c0c0000002f7f367e3e9461b8c6c2d919f276a0e344e673dfc84da21b8ff9606fa64f4429050000009ca17ff3474487cfc718e871696bd1d4d7669efced04aa5cdb4cc69f906da639000000009ca17ff3474487cfc718e871696bd1d4d7669efced04aa5cdb4cc69f906da639010000002e35829452aae4996c47ead73b20183bad78e83cb29185f4be9ca60b80e8c06208000000e9ec07776bc2337daa6ee7549b9ba38d8782f0581a21555bc9dde8156c713d6c080000009ba8c0f9ddc2531b576244930e75e66752e8a3dd70d3262ee1b379f58d19ba7d06000000efb7d48484544cfa06585494b427862f098708707ffa1610b4ed33a724bfe4d002000000efb7d48484544cfa06585494b427862f098708707ffa1610b4ed33a724bfe4d00300000042f2373701b15830d8fba26001b4480963db6cf238936eb8d6705ec3fe4cc9f801000000ce99bf405ecee24979e50a7e2a5eba7616269ef62c805a444e5dd6016c63cae72ed3582b963bbda2fdec219ff84299c525dfadcf4b69218a1400000000000000b0f624683b04c01ad58bcf6233f6e492faf0e7f9adf0609dab04a97796aabf13175262c74056690610c075d8cce2cc9f12549b8d5ab3484be19b9c3ebcc9c29dd8d8762bb6e940cc79acfc2940f709a3fa5ff0be11c1304c219931ce58dbac95", + quorum_hash: "000000000000000dce79f58fadb53bb32ada6c6f817b1e96e0b95a276248fa7a", + quorum_public_key: "a1ce5102d30de044adccb2a64d1120db355d222a1478b5e2a3f4bf7f90895ac01eccde2ff8cec27c9a886bdfa0b83b1a", + correct_signature: false, + },TestCase{ + instant_lock_hex: "010c828b35aa1ba4ec8ff7b34335f8d4179080f2ed3a0d63f349b8adc868c73ff201010000007a46cb5ef53943cec615f9054acc460418129e4f13ee33cc1f5c39707c11401b020000005d63eaab8d9d29e48e041b9f5b43e3e41b5dac21cd998e703b631198b77b731c0c0000002f7f367e3e9461b8c6c2d919f276a0e344e673dfc84da21b8ff9606fa64f4429050000009ca17ff3474487cfc718e871696bd1d4d7669efced04aa5cdb4cc69f906da639000000009ca17ff3474487cfc718e871696bd1d4d7669efced04aa5cdb4cc69f906da639010000002e35829452aae4996c47ead73b20183bad78e83cb29185f4be9ca60b80e8c06208000000e9ec07776bc2337daa6ee7549b9ba38d8782f0581a21555bc9dde8156c713d6c080000009ba8c0f9ddc2531b576244930e75e66752e8a3dd70d3262ee1b379f58d19ba7d06000000efb7d48484544cfa06585494b427862f098708707ffa1610b4ed33a724bfe4d002000000efb7d48484544cfa06585494b427862f098708707ffa1610b4ed33a724bfe4d00300000042f2373701b15830d8fba26001b4480963db6cf238936eb8d6705ec3fe4cc9f801000000ce99bf405ecee24979e50a7e2a5eba7616269ef62c805a444e5dd6016c63cae72ed3582b963bbda2fdec219ff84299c525dfadcf4b69218a1400000000000000b0f624683b04c01ad58bcf6233f6e492faf0e7f9adf0609dab04a97796aabf13175262c74056690610c075d8cce2cc9f12549b8d5ab3484be19b9c3ebcc9c29dd8d8762bb6e940cc79acfc2940f709a3fa5ff0be11c1304c219931ce58dbac95", + quorum_hash: "000000000000000e1fd2afb1f179b1f5c2f50a3614da93c0e40c4a1ddd921200", + quorum_public_key: "aa3d8543ec8e228b548ddcdee60de91b3bfdb1639b5d4ce295c9e8397985e14ebf2a0c371fb9b4d92354589af5ea2683", + correct_signature: false, + }, + ]; + + for tc in test_cases { + let hex_decoded = hex::decode(tc.instant_lock_hex).unwrap(); + let instant_lock: InstantLock = deserialize(&hex_decoded).unwrap(); + let quorum_hash = hex::decode(tc.quorum_hash).expect("correct hash"); + let quorum_public_key: [u8; 48] = hex::decode(tc.quorum_public_key) + .expect("correct public key") + .try_into() + .expect("correct public key len"); + + assert_eq!( + verify_instant_lock_signature( + &instant_lock, + &INSTANT_SEND_LOCK_QUORUM_TYPE, + &quorum_hash, + &quorum_public_key, + ) + .expect("signature verification failed"), + tc.correct_signature + ); + } + } +} diff --git a/packages/rs-sdk/src/platform/transition/put_identity.rs b/packages/rs-sdk/src/platform/transition/put_identity.rs index ce79b52d81c..d2a4475a19f 100644 --- a/packages/rs-sdk/src/platform/transition/put_identity.rs +++ b/packages/rs-sdk/src/platform/transition/put_identity.rs @@ -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}; diff --git a/packages/rs-sdk/src/platform/types/epoch.rs b/packages/rs-sdk/src/platform/types/epoch.rs index f6b86b77eeb..968192adf1e 100644 --- a/packages/rs-sdk/src/platform/types/epoch.rs +++ b/packages/rs-sdk/src/platform/types/epoch.rs @@ -9,7 +9,7 @@ use crate::{ Error, Sdk, }; -/// Epoch type used in the SDK. +/// Epoch information pub type Epoch = ExtendedEpochInfo; #[async_trait] diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index 699165e5369..886ccd5cd7c 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -5,6 +5,7 @@ use crate::internal_cache::InternalSdkCache; use crate::mock::MockResponse; #[cfg(feature = "mocks")] use crate::mock::{provider::GrpcContextProvider, MockDashPlatformSdk}; +use crate::networks::{NetworkType, QuorumParams}; use crate::platform::transition::put_settings::PutSettings; use crate::platform::{Fetch, Identifier}; use arc_swap::{ArcSwapAny, ArcSwapOption}; @@ -37,7 +38,6 @@ use std::fmt::Debug; use std::num::NonZeroUsize; #[cfg(feature = "mocks")] use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::atomic::Ordering; use std::sync::{atomic, Arc}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -98,7 +98,9 @@ pub struct Sdk { /// The network that the sdk is configured for (Dash (mainnet), Testnet, Devnet, Regtest) pub network: Network, inner: SdkInstance, - /// Use proofs when retrieving data from Platform. + /// Type of network we use. Determines some parameters, like quorum types. + network_type: NetworkType, + /// Use proofs when retrieving data from the platform. /// /// This is set to `true` by default. `false` is not implemented yet. proofs: bool, @@ -150,6 +152,7 @@ impl Clone for Sdk { metadata_height_tolerance: self.metadata_height_tolerance, metadata_time_tolerance_ms: self.metadata_time_tolerance_ms, dapi_client_settings: self.dapi_client_settings, + network_type: self.network_type.clone(), #[cfg(feature = "mocks")] dump_dir: self.dump_dir.clone(), } @@ -528,6 +531,11 @@ impl Sdk { } } + /// Return configuration of quorum, like type of quorum used for instant lock. + pub(crate) fn quorum_params(&self) -> QuorumParams { + self.network_type.to_quorum_params() + } + /// Return [Dash Platform version](PlatformVersion) information used by this SDK. /// /// @@ -707,8 +715,9 @@ impl DapiRequestExecutor for Sdk { /// Mandatory steps of initialization in normal mode are: /// /// 1. Create an instance of [SdkBuilder] with [`SdkBuilder::new()`] -/// 2. Configure the builder with [`SdkBuilder::with_core()`] -/// 3. Call [`SdkBuilder::build()`] to create the [Sdk] instance. +/// 2. Set up network type with [`SdkBuilder::with_network_type()`] (not needed for mock) +/// 3. Configure the builder with [`SdkBuilder::with_core()`] +/// 4. Call [`SdkBuilder::build()`] to create the [Sdk] instance. pub struct SdkBuilder { /// List of addressses to connect to. /// @@ -723,6 +732,8 @@ pub struct SdkBuilder { core_user: String, core_password: Zeroizing, + network_type: NetworkType, + /// If true, request and verify proofs of the responses. proofs: bool, @@ -774,6 +785,7 @@ impl Default for SdkBuilder { core_port: 0, core_password: "".to_string().into(), core_user: "".to_string(), + network_type: NetworkType::Mock, proofs: true, metadata_height_tolerance: Some(1), @@ -802,6 +814,10 @@ impl Default for SdkBuilder { impl SdkBuilder { /// Create a new SdkBuilder with provided address list. + /// + /// It creates new SdkBuilder, preconfigured to connect to provided addresses. + /// + /// Once created, you need to set [NetworkType] with [`SdkBuilder::with_network_type()`]. pub fn new(addresses: AddressList) -> Self { Self { addresses: Some(addresses), @@ -933,6 +949,15 @@ impl SdkBuilder { self } + /// Define network to which you want to connect. + /// + /// For development, you can use [NetworkType::Testnet] or [NetworkType::Devnet]. + /// For production, use [NetworkType::Mainnet]. + pub fn with_network_type(mut self, network_type: NetworkType) -> Self { + self.network_type = network_type; + self + } + /// Use Dash Core as a wallet and context provider. /// /// This is a convenience method that configures the SDK to use Dash Core as a wallet and context provider. @@ -1020,6 +1045,12 @@ impl SdkBuilder { let sdk= match self.addresses { // non-mock mode Some(addresses) => { + if self.network_type == NetworkType::Mock { + return Err(Error::Config( + "Network type must be set, use SdkBuilder::with_network_type()".to_string(), + )); + } + #[allow(unused_mut)] // needs to be mutable for features other than wasm let mut dapi = DapiClient::new(addresses, dapi_client_settings); #[cfg(not(target_arch = "wasm32"))] @@ -1035,6 +1066,7 @@ impl SdkBuilder { network: self.network, dapi_client_settings, inner:SdkInstance::Dapi { dapi, version:self.version }, + network_type: self.network_type, proofs:self.proofs, context_provider: ArcSwapOption::new( self.context_provider.map(Arc::new)), cancel_token: self.cancel_token, @@ -1103,6 +1135,7 @@ impl SdkBuilder { address_list: AddressList::new(), version: self.version, }, + network_type: self.network_type, dump_dir: self.dump_dir.clone(), proofs:self.proofs, internal_cache: Default::default(), diff --git a/packages/rs-sdk/tests/fetch/asset_lock.rs b/packages/rs-sdk/tests/fetch/asset_lock.rs new file mode 100644 index 00000000000..84186c1fc95 --- /dev/null +++ b/packages/rs-sdk/tests/fetch/asset_lock.rs @@ -0,0 +1,112 @@ +use dapi_grpc::platform::v0::get_epochs_info_request::GetEpochsInfoRequestV0; +use dapi_grpc::platform::v0::{GetEpochsInfoRequest, GetEpochsInfoResponse}; +use dapi_grpc::platform::VersionedGrpcResponse; +use dash_sdk::platform::transition::asset_lock::AssetLockProofVerifier; +use dpp::dashcore::consensus::deserialize; +use dpp::dashcore::hash_types::CycleHash; +use dpp::dashcore::hashes::hex::FromHex; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::{InstantLock, Transaction}; +use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; +use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; +use dpp::prelude::AssetLockProof; +use rs_dapi_client::{DapiRequest, IntoInner}; + +use super::{common::setup_logs, config::Config}; + +async fn current_platform_state(sdk: &dash_sdk::Sdk) -> (u32, Vec) { + let req: GetEpochsInfoRequest = GetEpochsInfoRequestV0 { + ascending: false, + count: 1, + prove: true, + start_epoch: None, + } + .into(); + + let resp: GetEpochsInfoResponse = req + .execute(sdk, Default::default()) + .await + .expect("get epoch info") + .into_inner(); + let core_height = resp.metadata().expect("metadata").core_chain_locked_height; + let quorum_hash = resp.proof().expect("proof").quorum_hash.clone(); + (core_height, quorum_hash) +} + +/// Given some existing identity ID, when I fetch the identity, and I get it. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_asset_lock_proof() { + setup_logs(); + + let cfg = Config::new(); + + let sdk = cfg.setup_api("test_asset_lock_proof").await; + let (core_chain_locked_height, quorum_hash) = current_platform_state(&sdk).await; + + // some semi-correct instant lock + let cyclehash = CycleHash::from_slice(&quorum_hash).expect("cycle hash"); + let instant_lock = InstantLock { + cyclehash, + ..Default::default() + }; + + let out_point = [0u8; 36]; + + // some hardcoded tx, just for tests + let tx_bytes = Vec::from_hex( + "010000000001000100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000" + ).unwrap(); + let tx: Transaction = deserialize(&tx_bytes).expect("deserialize tx"); + + struct TestCase { + asset_lock_proof: AssetLockProof, + // expect err that can be retried + expect_err: bool, + } + // instant_lock: InstantLock, transaction: Transaction, output_index: u32 + let test_cases = vec![ + TestCase { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof::new( + core_chain_locked_height, + out_point, + )), + expect_err: false, + }, + TestCase { + asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::new( + instant_lock, + tx.clone(), + 0, + )), + expect_err: false, + }, + TestCase { + asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::new( + InstantLock::default(), + tx, + 0, + )), + expect_err: true, + }, + TestCase { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof::new( + core_chain_locked_height + 100, + out_point, + )), + expect_err: true, + }, + ]; + + for (i, tc) in test_cases.into_iter().enumerate() { + let result = tc.asset_lock_proof.verify(&sdk).await; + assert_eq!( + result.is_err(), + tc.expect_err, + "tc {} expeced err = {}, got err = {}: {:?}", + i, + tc.expect_err, + result.is_err(), + result + ); + } +} diff --git a/packages/rs-sdk/tests/fetch/config.rs b/packages/rs-sdk/tests/fetch/config.rs index a93eb2402c5..b8031f87510 100644 --- a/packages/rs-sdk/tests/fetch/config.rs +++ b/packages/rs-sdk/tests/fetch/config.rs @@ -3,6 +3,7 @@ //! This module contains [Config] struct that can be used to configure dash-platform-sdk. //! It's mainly used for testing. +use dash_sdk::networks::NetworkType; use dpp::platform_value::string_encoding::Encoding; use dpp::{ dashcore::{hashes::Hash, ProTxHash}, diff --git a/packages/rs-sdk/tests/fetch/mod.rs b/packages/rs-sdk/tests/fetch/mod.rs index 363e35f069f..9e89b2160b2 100644 --- a/packages/rs-sdk/tests/fetch/mod.rs +++ b/packages/rs-sdk/tests/fetch/mod.rs @@ -5,6 +5,7 @@ compile_error!("tests require `mocks` feature to be enabled"); #[cfg(not(any(feature = "network-testing", feature = "offline-testing")))] compile_error!("network-testing or offline-testing must be enabled for tests"); +mod asset_lock; #[cfg(feature = "mocks")] mod broadcast; mod common; diff --git a/packages/rs-sdk/tests/vectors/test_asset_lock_proof/.gitkeep b/packages/rs-sdk/tests/vectors/test_asset_lock_proof/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rs-sdk/tests/vectors/test_asset_lock_proof/msg_GetEpochsInfoRequest_1b87e649557ccb609adb9e2904c67089535588985622579e77969e0ffd68afc7.json b/packages/rs-sdk/tests/vectors/test_asset_lock_proof/msg_GetEpochsInfoRequest_1b87e649557ccb609adb9e2904c67089535588985622579e77969e0ffd68afc7.json new file mode 100644 index 00000000000..5d6f3e56307 Binary files /dev/null and b/packages/rs-sdk/tests/vectors/test_asset_lock_proof/msg_GetEpochsInfoRequest_1b87e649557ccb609adb9e2904c67089535588985622579e77969e0ffd68afc7.json differ diff --git a/packages/rs-sdk/tests/vectors/test_asset_lock_proof/quorum_pubkey-100-4ce7fd81273c2b394c0f32367374fc5b09ba912e017aacb366d2171e9ca6f9d5.json b/packages/rs-sdk/tests/vectors/test_asset_lock_proof/quorum_pubkey-100-4ce7fd81273c2b394c0f32367374fc5b09ba912e017aacb366d2171e9ca6f9d5.json new file mode 100644 index 00000000000..eecd3bda41e --- /dev/null +++ b/packages/rs-sdk/tests/vectors/test_asset_lock_proof/quorum_pubkey-100-4ce7fd81273c2b394c0f32367374fc5b09ba912e017aacb366d2171e9ca6f9d5.json @@ -0,0 +1 @@ +8aa46461c5a7e1b5da330050d97b3dc928445c3908c2b0f9d3b1b84fd4a7a2ecdd2da5e7480690b0f0f5e10ae51555a7 \ No newline at end of file