diff --git a/Cargo.lock b/Cargo.lock index 536b0d6f4..44c91da7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9591,6 +9591,25 @@ dependencies = [ "sp-application-crypto", ] +[[package]] +name = "sp-native-token-management" +version = "0.1.0" +dependencies = [ + "async-trait", + "main-chain-follower-api", + "parity-scale-codec", + "scale-info", + "serde", + "sidechain-domain", + "sidechain-mc-hash", + "sp-api", + "sp-blockchain", + "sp-inherents", + "sp-runtime", + "thiserror", + "tokio", +] + [[package]] name = "sp-offchain" version = "34.0.0" diff --git a/primitives/native-token-management/Cargo.toml b/primitives/native-token-management/Cargo.toml new file mode 100644 index 000000000..e1d35f2c4 --- /dev/null +++ b/primitives/native-token-management/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "sp-native-token-management" +version = "0.1.0" +edition = "2021" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +async-trait = { workspace = true, optional = true } +main-chain-follower-api = { workspace = true, optional = true, features = ["native-token"] } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +sidechain-domain = { workspace = true } +sidechain-mc-hash = { workspace = true, optional = true } +sp-api = { workspace = true } +sp-blockchain = { workspace = true, optional = true } +sp-inherents = { workspace = true } +sp-runtime = { workspace = true } +thiserror = { workspace = true, optional = true } +serde = { workspace = true, optional = true } + +[dev-dependencies] +tokio = { workspace = true } + +[features] +default = ["std"] +std = [ + "async-trait", + "main-chain-follower-api/std", + "parity-scale-codec/std", + "scale-info/std", + "sidechain-domain/std", + "sidechain-mc-hash", + "sp-api/std", + "sp-blockchain", + "sp-inherents/std", + "sp-runtime/std", + "thiserror" +] +serde = [ + "dep:serde", + "scale-info/serde", + "sidechain-domain/serde", +] diff --git a/primitives/native-token-management/src/lib.rs b/primitives/native-token-management/src/lib.rs new file mode 100644 index 000000000..f82a37a8f --- /dev/null +++ b/primitives/native-token-management/src/lib.rs @@ -0,0 +1,127 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +pub use inherent_provider::*; + +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use sidechain_domain::*; +use sp_inherents::*; +use sp_runtime::{ + scale_info::TypeInfo, + traits::{Block as BlockT, Header, Zero}, +}; + +#[cfg(test)] +mod tests; + +pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"ntreserv"; + +#[derive(Default, Debug, Clone, PartialEq, Eq, TypeInfo, Encode, Decode, MaxEncodedLen)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MainChainScripts { + pub native_token_policy: PolicyId, + pub native_token_asset_name: AssetName, + pub illiquid_supply_address: MainchainAddress, +} + +sp_api::decl_runtime_apis! { + pub trait NativeTokenManagementApi { + fn get_main_chain_scripts() -> MainChainScripts; + } +} + +#[derive(Decode, Encode)] +pub struct TokenTransferData { + pub token_amount: NativeTokenAmount, +} + +#[cfg(feature = "std")] +mod inherent_provider { + use super::*; + use main_chain_follower_api::{DataSourceError, NativeTokenManagementDataSource}; + use sidechain_mc_hash::McHashInherentDigest; + use sp_api::{ApiError, ProvideRuntimeApi}; + use sp_blockchain::HeaderBackend; + use std::sync::Arc; + + pub struct NativeTokenManagementInherentDataProvider { + pub token_amount: NativeTokenAmount, + } + + #[derive(thiserror::Error, sp_runtime::RuntimeDebug)] + pub enum IDPCreationError { + #[error("Failed to read native token data from data source: {0:?}")] + DataSourceError(DataSourceError), + #[error("Failed to retrieve main chain scripts from the runtime: {0:?}")] + GetMainChainScriptsError(ApiError), + } + + impl From for IDPCreationError { + fn from(err: DataSourceError) -> Self { + Self::DataSourceError(err) + } + } + + impl NativeTokenManagementInherentDataProvider { + pub async fn new( + client: Arc, + data_source: &(dyn NativeTokenManagementDataSource + Send + Sync), + mc_hash: McBlockHash, + parent_hash: ::Hash, + ) -> Result + where + Block: BlockT, + C: HeaderBackend, + C: ProvideRuntimeApi + Send + Sync, + C::Api: NativeTokenManagementApi, + { + let api = client.runtime_api(); + let scripts = api + .get_main_chain_scripts(parent_hash) + .map_err(IDPCreationError::GetMainChainScriptsError)?; + let parent_mc_hash: Option = client + .header(parent_hash) + .unwrap() + .filter(|parent_header| !parent_header.number().is_zero()) + .map(|parent_header| { + McHashInherentDigest::value_from_digest(&parent_header.digest().logs).unwrap() + }); + let token_amount = data_source + .get_token_transfer_events( + parent_mc_hash, + mc_hash, + scripts.native_token_policy, + scripts.native_token_asset_name, + scripts.illiquid_supply_address, + ) + .await?; + + Ok(Self { token_amount }) + } + } + + #[async_trait::async_trait] + impl InherentDataProvider for NativeTokenManagementInherentDataProvider { + async fn provide_inherent_data( + &self, + inherent_data: &mut InherentData, + ) -> Result<(), sp_inherents::Error> { + inherent_data.put_data( + INHERENT_IDENTIFIER, + &TokenTransferData { token_amount: self.token_amount.clone() }, + ) + } + + async fn try_handle_error( + &self, + identifier: &InherentIdentifier, + _error: &[u8], + ) -> Option> { + if *identifier == INHERENT_IDENTIFIER { + panic!("BUG: {:?} inherent shouldn't return any errors", INHERENT_IDENTIFIER) + } else { + None + } + } + } +} diff --git a/primitives/native-token-management/src/tests/mod.rs b/primitives/native-token-management/src/tests/mod.rs new file mode 100644 index 000000000..32ce3d920 --- /dev/null +++ b/primitives/native-token-management/src/tests/mod.rs @@ -0,0 +1,148 @@ +pub(crate) mod runtime_api_mock; + +#[cfg(feature = "std")] +mod inherent_provider { + use super::runtime_api_mock::*; + use crate::inherent_provider::*; + use crate::INHERENT_IDENTIFIER; + use main_chain_follower_api::mock_services::MockNativeTokenDataSource; + use sidechain_domain::*; + use sidechain_mc_hash::MC_HASH_DIGEST_ID; + use sp_inherents::InherentData; + use sp_inherents::InherentDataProvider; + use sp_runtime::testing::Digest; + use sp_runtime::testing::DigestItem; + use std::sync::Arc; + + #[tokio::test] + async fn correctly_fetches_total_transfer_between_two_hashes() { + let parent_number = 1; // not genesis + + let mc_hash = McBlockHash([1; 32]); + let parent_hash = Hash::from([2; 32]); + let parent_mc_hash = Some(McBlockHash([3; 32])); + let total_transfered = 103; + + let data_source = + create_data_source(parent_mc_hash.clone(), mc_hash.clone(), total_transfered); + let client = create_client(parent_hash, parent_mc_hash, parent_number); + + let inherent_provider = NativeTokenManagementInherentDataProvider::new( + client, + &data_source, + mc_hash, + parent_hash, + ) + .await + .expect("Should not fail"); + + assert_eq!(inherent_provider.token_amount.0, total_transfered) + } + + #[tokio::test] + async fn fetches_with_no_lower_bound_when_parent_is_genesis() { + let parent_number = 0; // genesis + + let mc_hash = McBlockHash([1; 32]); + let parent_hash = Hash::from([2; 32]); + let parent_mc_hash = None; // genesis doesn't refer to any mc hash + let total_transfered = 103; + + let data_source = + create_data_source(parent_mc_hash.clone(), mc_hash.clone(), total_transfered); + let client = create_client(parent_hash, parent_mc_hash, parent_number); + + let inherent_provider = NativeTokenManagementInherentDataProvider::new( + client, + &data_source, + mc_hash, + parent_hash, + ) + .await + .expect("Should not fail"); + + assert_eq!(inherent_provider.token_amount.0, total_transfered) + } + + #[tokio::test] + async fn defaults_to_zero_when_no_data() { + let parent_number = 1; + + let mc_hash = McBlockHash([1; 32]); + let parent_hash = Hash::from([2; 32]); + let parent_mc_hash = Some(McBlockHash([3; 32])); + + let data_source = MockNativeTokenDataSource::new([].into()); + let client = create_client(parent_hash, parent_mc_hash, parent_number); + + let inherent_provider = NativeTokenManagementInherentDataProvider::new( + client, + &data_source, + mc_hash, + parent_hash, + ) + .await + .expect("Should not fail"); + + assert_eq!(inherent_provider.token_amount.0, 0) + } + + #[tokio::test] + async fn correctly_puts_data_into_inherent_data_structure() { + let token_amount = 1234; + + let mut inherent_data = InherentData::new(); + + let inherent_provider = NativeTokenManagementInherentDataProvider { + token_amount: NativeTokenAmount(token_amount), + }; + + inherent_provider.provide_inherent_data(&mut inherent_data).await.unwrap(); + + assert_eq!( + inherent_data + .get_data::(&INHERENT_IDENTIFIER) + .unwrap() + .unwrap() + .0, + token_amount + ) + } + + fn create_data_source( + parent_mc_hash: Option, + mc_hash: McBlockHash, + total_transfered: u64, + ) -> MockNativeTokenDataSource { + let total_transfered = NativeTokenAmount(total_transfered); + MockNativeTokenDataSource::new([((parent_mc_hash, mc_hash), total_transfered)].into()) + } + + fn create_client( + parent_hash: Hash, + parent_mc_hash: Option, + parent_number: u32, + ) -> Arc { + Arc::new(TestApi { + headers: [( + parent_hash.clone(), + Header { + digest: Digest { + logs: match parent_mc_hash { + None => vec![], + Some(parent_mc_hash) => vec![DigestItem::PreRuntime( + MC_HASH_DIGEST_ID, + parent_mc_hash.0.to_vec(), + )], + }, + }, + extrinsics_root: Default::default(), + number: parent_number, + parent_hash: parent_hash.clone(), + state_root: Default::default(), + }, + )] + .into(), + }) + } +} diff --git a/primitives/native-token-management/src/tests/runtime_api_mock.rs b/primitives/native-token-management/src/tests/runtime_api_mock.rs new file mode 100644 index 000000000..4d21420c2 --- /dev/null +++ b/primitives/native-token-management/src/tests/runtime_api_mock.rs @@ -0,0 +1,78 @@ +use sp_blockchain::HeaderBackend; +use sp_runtime::traits::{Block as BlockT, NumberFor, Zero}; +use std::collections::HashMap; + +use crate::MainChainScripts; + +pub type Block = sp_runtime::generic::Block< + sp_runtime::generic::Header, + sp_runtime::OpaqueExtrinsic, +>; + +pub type Hash = ::Hash; +pub type Header = ::Header; + +#[derive(Clone)] +pub struct TestApi { + pub headers: HashMap<::Hash, ::Header>, +} + +impl sp_api::ProvideRuntimeApi for TestApi { + type Api = TestApi; + + fn runtime_api(&self) -> sp_api::ApiRef { + self.clone().into() + } +} + +sp_api::mock_impl_runtime_apis! { + impl crate::NativeTokenManagementApi for TestApi { + fn get_main_chain_scripts() -> MainChainScripts { + MainChainScripts::default() + } + + } +} + +impl HeaderBackend for TestApi { + fn header( + &self, + id: ::Hash, + ) -> Result::Header>, sp_blockchain::Error> { + Ok(self.headers.get(&id).cloned()) + } + + fn info(&self) -> sp_blockchain::Info { + sp_blockchain::Info { + best_hash: Default::default(), + best_number: Zero::zero(), + finalized_hash: Default::default(), + finalized_number: Zero::zero(), + genesis_hash: Default::default(), + number_leaves: Default::default(), + finalized_state: None, + block_gap: None, + } + } + + fn status( + &self, + _id: ::Hash, + ) -> Result { + Ok(sp_blockchain::BlockStatus::Unknown) + } + + fn number( + &self, + _hash: ::Hash, + ) -> Result>, sp_blockchain::Error> { + Ok(None) + } + + fn hash( + &self, + _number: NumberFor, + ) -> Result::Hash>, sp_blockchain::Error> { + unimplemented!() + } +} diff --git a/primitives/sidechain-mc-hash/Cargo.toml b/primitives/sidechain-mc-hash/Cargo.toml index 163ff4c0e..1b7de1b08 100644 --- a/primitives/sidechain-mc-hash/Cargo.toml +++ b/primitives/sidechain-mc-hash/Cargo.toml @@ -7,7 +7,7 @@ description = "Logic for putting a main chain block reference in digest and inhe [dependencies] async-trait = { workspace = true } main-chain-follower-api = { workspace = true, features = ["block-source"] } -sp-consensus-slots = { workspace = true } +sp-consensus-slots = { workspace = true, features = ["std"] } sidechain-domain = { workspace = true, features = ["std"] } sp-consensus = { workspace = true } sp-inherents = { workspace = true, features = ["std"] } diff --git a/primitives/sidechain-mc-hash/src/lib.rs b/primitives/sidechain-mc-hash/src/lib.rs index 56cdb3c97..7fe79f662 100644 --- a/primitives/sidechain-mc-hash/src/lib.rs +++ b/primitives/sidechain-mc-hash/src/lib.rs @@ -13,7 +13,7 @@ use std::{error::Error, ops::Deref}; mod test; pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"scmchash"; -const MC_HASH_DIGEST_ID: [u8; 4] = *b"mcsh"; +pub const MC_HASH_DIGEST_ID: [u8; 4] = *b"mcsh"; #[derive(Debug)] pub struct McHashInherentDataProvider { @@ -119,6 +119,10 @@ impl McHashInherentDataProvider { pub fn mc_block(&self) -> McBlockNumber { self.mc_block.number } + + pub fn mc_hash(&self) -> McBlockHash { + self.mc_block.hash.clone() + } } async fn get_mc_state_reference(