diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index 10c6ce83..e90c6f62 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "serai-client" version = "0.1.0" -description = "Client library for the Serai network" +description = "A client for Serai and its connected networks" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client" authors = ["Luke Parker "] @@ -17,58 +17,17 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] -zeroize = "^1.5" -thiserror = { version = "2", default-features = false, optional = true } - -bitvec = { version = "1", default-features = false, features = ["alloc", "serde"] } - -hex = "0.4" -scale = { package = "parity-scale-codec", version = "3", optional = true } -borsh = { version = "1", features = ["derive"] } -serde = { version = "1", features = ["derive"], optional = true } -serde_json = { version = "1", optional = true } - -serai-abi = { path = "../abi", version = "0.1" } - -multiaddr = { version = "0.18", optional = true } -sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true } -sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true } -frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true } - -async-lock = "3" - -simple-request = { path = "../../common/request", version = "0.3", optional = true } +serai-client-serai = { path = "./serai", optional = true } serai-client-bitcoin = { path = "./bitcoin", optional = true } serai-client-ethereum = { path = "./ethereum", optional = true } serai-client-monero = { path = "./monero", optional = true } -[dev-dependencies] -rand_core = "0.6" -hex = "0.4" - -blake2 = "0.11.0-rc.0" - -ciphersuite = { path = "../../crypto/ciphersuite" } -dalek-ff-group = { path = "../../crypto/dalek-ff-group" } -ciphersuite-kp256 = { path = "../../crypto/ciphersuite/kp256" } -dkg-musig = { path = "../../crypto/dkg/musig" } -frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] } -schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" } - -tokio = "1" - -dockertest = "0.5" -serai-docker-tests = { path = "../../tests/docker" } - [features] -serai = ["thiserror/std", "scale", "serde", "serde_json", "multiaddr", "sp-core", "sp-runtime", "frame-system", "simple-request"] +serai = ["serai-client-serai"] -networks = [] -bitcoin = ["networks", "serai-client-bitcoin"] -ethereum = ["networks", "serai-client-ethereum"] -monero = ["networks", "serai-client-monero"] +bitcoin = ["serai-client-bitcoin"] +ethereum = ["serai-client-ethereum"] +monero = ["serai-client-monero"] -# Assumes the default usage is to use Serai as a DEX, which doesn't actually -# require connecting to a Serai node -default = ["bitcoin", "ethereum", "monero"] +default = ["serai", "bitcoin", "ethereum", "monero"] diff --git a/substrate/client/README.md b/substrate/client/README.md new file mode 100644 index 00000000..8900f637 --- /dev/null +++ b/substrate/client/README.md @@ -0,0 +1,4 @@ +# serai-client + +This crate is an umbrella crate for each of Serai's clients (the network itself +and its connected networks). diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index f0ee54d0..8435b13e 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -1,12 +1,17 @@ -#[cfg(feature = "bitcoin")] -pub use serai_client_bitcoin as bitcoin; -#[cfg(feature = "ethereum")] -pub mod serai_client_ethereum as ethereum; -#[cfg(feature = "monero")] -pub mod serai_client_monero as monero; +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +/// The client for the Serai network itself. #[cfg(feature = "serai")] pub use serai_client_serai as serai; -#[cfg(test)] -mod tests; +/// The client for the Bitcoin integration. +#[cfg(feature = "bitcoin")] +pub use serai_client_bitcoin as bitcoin; +/// The client for the Ethereum integration. +#[cfg(feature = "ethereum")] +pub use serai_client_ethereum as ethereum; +/// The client for the Monero integration. +#[cfg(feature = "monero")] +pub use serai_client_monero as monero; diff --git a/substrate/client/src/serai/coins.rs b/substrate/client/src/serai/coins.rs deleted file mode 100644 index 2da598fd..00000000 --- a/substrate/client/src/serai/coins.rs +++ /dev/null @@ -1,83 +0,0 @@ -use scale::Encode; - -use serai_abi::primitives::{SeraiAddress, Amount, Coin, Balance}; -pub use serai_abi::coins::primitives; -use primitives::OutInstructionWithBalance; - -use crate::{TemporalSerai, SeraiError}; - -const PALLET: &str = "Coins"; - -pub type CoinsEvent = serai_abi::coins::Event; - -#[derive(Clone, Copy)] -pub struct SeraiCoins<'a>(pub(crate) &'a TemporalSerai<'a>); -impl SeraiCoins<'_> { - pub async fn mint_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::Coins(event) = event { - if matches!(event, CoinsEvent::Mint { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub async fn burn_with_instruction_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::Coins(event) = event { - if matches!(event, CoinsEvent::BurnWithInstruction { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub async fn coin_supply(&self, coin: Coin) -> Result { - Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0))) - } - - pub async fn coin_balance( - &self, - coin: Coin, - address: SeraiAddress, - ) -> Result { - Ok( - self - .0 - .storage( - PALLET, - "Balances", - (sp_core::hashing::blake2_128(&address.encode()), &address.0, coin), - ) - .await? - .unwrap_or(Amount(0)), - ) - } - - pub fn transfer(to: SeraiAddress, balance: Balance) -> serai_abi::Call { - serai_abi::Call::Coins(serai_abi::coins::Call::transfer { to, balance }) - } - - pub fn burn(balance: Balance) -> serai_abi::Call { - serai_abi::Call::Coins(serai_abi::coins::Call::burn { balance }) - } - - pub fn burn_with_instruction(instruction: OutInstructionWithBalance) -> serai_abi::Call { - serai_abi::Call::Coins(serai_abi::coins::Call::burn_with_instruction { instruction }) - } -} diff --git a/substrate/client/src/serai/dex.rs b/substrate/client/src/serai/dex.rs deleted file mode 100644 index 88ccf3f8..00000000 --- a/substrate/client/src/serai/dex.rs +++ /dev/null @@ -1,74 +0,0 @@ -use sp_core::bounded::BoundedVec; -use serai_abi::primitives::{Amount, Coin, ExternalCoin, SeraiAddress}; - -use crate::{SeraiError, TemporalSerai}; - -pub type DexEvent = serai_abi::dex::Event; - -const PALLET: &str = "Dex"; - -#[derive(Clone, Copy)] -pub struct SeraiDex<'a>(pub(crate) &'a TemporalSerai<'a>); -impl SeraiDex<'_> { - pub async fn events(&self) -> Result, SeraiError> { - self - .0 - .events( - |event| if let serai_abi::Event::Dex(event) = event { Some(event.clone()) } else { None }, - ) - .await - } - - pub fn add_liquidity( - coin: ExternalCoin, - coin_amount: Amount, - sri_amount: Amount, - min_coin_amount: Amount, - min_sri_amount: Amount, - address: SeraiAddress, - ) -> serai_abi::Call { - serai_abi::Call::Dex(serai_abi::dex::Call::add_liquidity { - coin, - coin_desired: coin_amount.0, - sri_desired: sri_amount.0, - coin_min: min_coin_amount.0, - sri_min: min_sri_amount.0, - mint_to: address, - }) - } - - pub fn swap( - from_coin: Coin, - to_coin: Coin, - amount_in: Amount, - amount_out_min: Amount, - address: SeraiAddress, - ) -> serai_abi::Call { - let path = if to_coin.is_native() { - BoundedVec::try_from(vec![from_coin, Coin::Serai]).unwrap() - } else if from_coin.is_native() { - BoundedVec::try_from(vec![Coin::Serai, to_coin]).unwrap() - } else { - BoundedVec::try_from(vec![from_coin, Coin::Serai, to_coin]).unwrap() - }; - - serai_abi::Call::Dex(serai_abi::dex::Call::swap_exact_tokens_for_tokens { - path, - amount_in: amount_in.0, - amount_out_min: amount_out_min.0, - send_to: address, - }) - } - - /// Returns the reserves of `coin:SRI` pool. - pub async fn get_reserves( - &self, - coin: ExternalCoin, - ) -> Result, SeraiError> { - self.0.runtime_api("DexApi_get_reserves", (Coin::from(coin), Coin::Serai)).await - } - - pub async fn oracle_value(&self, coin: ExternalCoin) -> Result, SeraiError> { - self.0.storage(PALLET, "SecurityOracleValue", coin).await - } -} diff --git a/substrate/client/src/serai/genesis_liquidity.rs b/substrate/client/src/serai/genesis_liquidity.rs deleted file mode 100644 index fbbf0d6d..00000000 --- a/substrate/client/src/serai/genesis_liquidity.rs +++ /dev/null @@ -1,69 +0,0 @@ -pub use serai_abi::genesis_liquidity::primitives; -use primitives::{Values, LiquidityAmount}; - -use serai_abi::primitives::*; - -use sp_core::sr25519::Signature; - -use scale::Encode; - -use crate::{Serai, SeraiError, TemporalSerai, Transaction}; - -pub type GenesisLiquidityEvent = serai_abi::genesis_liquidity::Event; - -const PALLET: &str = "GenesisLiquidity"; - -#[derive(Clone, Copy)] -pub struct SeraiGenesisLiquidity<'a>(pub(crate) &'a TemporalSerai<'a>); -impl SeraiGenesisLiquidity<'_> { - pub async fn events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::GenesisLiquidity(event) = event { - Some(event.clone()) - } else { - None - } - }) - .await - } - - pub fn oraclize_values(values: Values, signature: Signature) -> Transaction { - Serai::unsigned(serai_abi::Call::GenesisLiquidity( - serai_abi::genesis_liquidity::Call::oraclize_values { values, signature }, - )) - } - - pub fn remove_coin_liquidity(balance: ExternalBalance) -> serai_abi::Call { - serai_abi::Call::GenesisLiquidity(serai_abi::genesis_liquidity::Call::remove_coin_liquidity { - balance, - }) - } - - pub async fn liquidity( - &self, - address: &SeraiAddress, - coin: ExternalCoin, - ) -> Result { - Ok( - self - .0 - .storage( - PALLET, - "Liquidity", - (coin, sp_core::hashing::blake2_128(&address.encode()), &address.0), - ) - .await? - .unwrap_or(LiquidityAmount::zero()), - ) - } - - pub async fn supply(&self, coin: ExternalCoin) -> Result { - Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero())) - } - - pub async fn genesis_complete_block(&self) -> Result, SeraiError> { - self.0.storage(PALLET, "GenesisCompleteBlock", ()).await - } -} diff --git a/substrate/client/src/serai/in_instructions.rs b/substrate/client/src/serai/in_instructions.rs deleted file mode 100644 index db9a4f78..00000000 --- a/substrate/client/src/serai/in_instructions.rs +++ /dev/null @@ -1,42 +0,0 @@ -pub use serai_abi::in_instructions::primitives; -use primitives::SignedBatch; - -use crate::{primitives::ExternalNetworkId, Transaction, SeraiError, Serai, TemporalSerai}; - -pub type InInstructionsEvent = serai_abi::in_instructions::Event; - -const PALLET: &str = "InInstructions"; - -#[derive(Clone, Copy)] -pub struct SeraiInInstructions<'a>(pub(crate) &'a TemporalSerai<'a>); -impl SeraiInInstructions<'_> { - pub async fn last_batch_for_network( - &self, - network: ExternalNetworkId, - ) -> Result, SeraiError> { - self.0.storage(PALLET, "LastBatch", network).await - } - - pub async fn batch_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::InInstructions(event) = event { - if matches!(event, InInstructionsEvent::Batch { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub fn execute_batch(batch: SignedBatch) -> Transaction { - Serai::unsigned(serai_abi::Call::InInstructions( - serai_abi::in_instructions::Call::execute_batch { batch }, - )) - } -} diff --git a/substrate/client/src/serai/liquidity_tokens.rs b/substrate/client/src/serai/liquidity_tokens.rs deleted file mode 100644 index e8706a64..00000000 --- a/substrate/client/src/serai/liquidity_tokens.rs +++ /dev/null @@ -1,46 +0,0 @@ -use scale::Encode; - -use serai_abi::primitives::{Amount, ExternalBalance, ExternalCoin, SeraiAddress}; - -use crate::{TemporalSerai, SeraiError}; - -const PALLET: &str = "LiquidityTokens"; - -#[derive(Clone, Copy)] -pub struct SeraiLiquidityTokens<'a>(pub(crate) &'a TemporalSerai<'a>); -impl SeraiLiquidityTokens<'_> { - pub async fn token_supply(&self, coin: ExternalCoin) -> Result { - Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0))) - } - - pub async fn token_balance( - &self, - coin: ExternalCoin, - address: SeraiAddress, - ) -> Result { - Ok( - self - .0 - .storage( - PALLET, - "Balances", - (sp_core::hashing::blake2_128(&address.encode()), &address.0, coin), - ) - .await? - .unwrap_or(Amount(0)), - ) - } - - pub fn transfer(to: SeraiAddress, balance: ExternalBalance) -> serai_abi::Call { - serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer { - to, - balance: balance.into(), - }) - } - - pub fn burn(balance: ExternalBalance) -> serai_abi::Call { - serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn { - balance: balance.into(), - }) - } -} diff --git a/substrate/client/src/serai/mod.rs b/substrate/client/src/serai/mod.rs deleted file mode 100644 index 02fed62e..00000000 --- a/substrate/client/src/serai/mod.rs +++ /dev/null @@ -1,434 +0,0 @@ -use thiserror::Error; - -use async_lock::RwLock; -use simple_request::{hyper, Request, Client}; - -use scale::{Decode, Encode}; -use serde::{Serialize, Deserialize, de::DeserializeOwned}; - -pub use sp_core::{ - Pair as PairTrait, - sr25519::{Public, Pair}, -}; - -pub use serai_abi as abi; -pub use abi::{primitives, Transaction}; -use abi::*; - -pub use primitives::{SeraiAddress, Signature, Amount}; -use primitives::{Header, ExternalNetworkId, QuotePriceParams}; -use crate::in_instructions::primitives::Shorthand; - -pub mod coins; -pub use coins::SeraiCoins; -pub mod dex; -pub use dex::SeraiDex; -pub mod in_instructions; -pub use in_instructions::SeraiInInstructions; -pub mod validator_sets; -pub use validator_sets::SeraiValidatorSets; -pub mod genesis_liquidity; -pub use genesis_liquidity::SeraiGenesisLiquidity; -pub mod liquidity_tokens; -pub use liquidity_tokens::SeraiLiquidityTokens; - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)] -pub struct Block { - pub header: Header, - pub transactions: Vec, -} -impl Block { - pub fn hash(&self) -> [u8; 32] { - self.header.hash().into() - } - pub fn number(&self) -> u64 { - self.header.number - } - - /// Returns the time of this block, set by its producer, in milliseconds since the epoch. - pub fn time(&self) -> Option { - for transaction in &self.transactions { - if let Call::Timestamp(timestamp::Call::set { now }) = transaction.call() { - return Some(*now); - } - } - None - } -} - -#[derive(Debug, Error)] -pub enum SeraiError { - #[error("failed to communicate with serai")] - ConnectionError, - #[error("node is faulty: {0}")] - InvalidNode(String), - #[error("error in response: {0}")] - ErrorInResponse(String), - #[error("serai-client library was intended for a different runtime version: {0}")] - InvalidRuntime(String), -} - -#[derive(Clone)] -pub struct Serai { - url: String, - client: Client, - genesis: [u8; 32], -} - -type EventsInBlock = Vec>; -pub struct TemporalSerai<'a> { - serai: &'a Serai, - block: [u8; 32], - events: RwLock>, -} -impl Clone for TemporalSerai<'_> { - fn clone(&self) -> Self { - Self { serai: self.serai, block: self.block, events: RwLock::new(None) } - } -} - -impl Serai { - pub async fn call( - &self, - method: &str, - params: Req, - ) -> Result { - let request = Request::from( - hyper::Request::post(&self.url) - .header("Content-Type", "application/json") - .body( - serde_json::to_vec( - &serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }), - ) - .unwrap() - .into(), - ) - .unwrap(), - ); - - #[derive(Deserialize)] - pub struct Error { - message: String, - } - - #[derive(Deserialize)] - #[serde(untagged)] - enum RpcResponse { - Ok { result: T }, - Err { error: Error }, - } - - let mut res = self - .client - .request(request) - .await - .map_err(|_| SeraiError::ConnectionError)? - .body() - .await - .map_err(|_| SeraiError::ConnectionError)?; - - let res: RpcResponse = serde_json::from_reader(&mut res).map_err(|e| { - SeraiError::InvalidRuntime(format!( - "response was a different type than expected: {:?}", - e.classify() - )) - })?; - match res { - RpcResponse::Ok { result } => Ok(result), - RpcResponse::Err { error } => Err(SeraiError::ErrorInResponse(error.message)), - } - } - - fn hex_decode(str: String) -> Result, SeraiError> { - (if let Some(stripped) = str.strip_prefix("0x") { - hex::decode(stripped) - } else { - hex::decode(str) - }) - .map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string())) - } - - pub async fn block_hash(&self, number: u64) -> Result, SeraiError> { - let hash: Option = self.call("chain_getBlockHash", [number]).await?; - let Some(hash) = hash else { return Ok(None) }; - Self::hex_decode(hash)? - .try_into() - .map_err(|_| SeraiError::InvalidNode("didn't respond to getBlockHash with hash".to_string())) - .map(Some) - } - - pub async fn new(url: String) -> Result { - let client = Client::with_connection_pool().map_err(|_| SeraiError::ConnectionError)?; - let mut res = Serai { url, client, genesis: [0xfe; 32] }; - res.genesis = res.block_hash(0).await?.ok_or_else(|| { - SeraiError::InvalidNode("node didn't have the first block's hash".to_string()) - })?; - Ok(res) - } - - fn unsigned(call: Call) -> Transaction { - Transaction::new(call, None) - } - - pub fn sign(&self, signer: &Pair, call: Call, nonce: u32, tip: u64) -> Transaction { - const SPEC_VERSION: u32 = 1; - const TX_VERSION: u32 = 1; - - let extra = Extra { era: sp_runtime::generic::Era::Immortal, nonce, tip }; - let signature_payload = ( - &call, - &extra, - SignedPayloadExtra { - spec_version: SPEC_VERSION, - tx_version: TX_VERSION, - genesis: self.genesis, - mortality_checkpoint: self.genesis, - }, - ) - .encode(); - let signature = signer.sign(&signature_payload); - - Transaction::new(call, Some((signer.public().into(), signature, extra))) - } - - pub async fn publish(&self, tx: &Transaction) -> Result<(), SeraiError> { - // Drop the returned hash, which is the hash of the raw extrinsic, as extrinsics are allowed - // to share hashes and this hash is accordingly useless/unsafe - // If we are to return something, it should be block included in and position within block - let _: String = self.call("author_submitExtrinsic", [hex::encode(tx.encode())]).await?; - Ok(()) - } - - pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> { - let hash: String = self.call("chain_getFinalizedHead", ()).await?; - Self::hex_decode(hash)?.try_into().map_err(|_| { - SeraiError::InvalidNode("didn't respond to getFinalizedHead with hash".to_string()) - }) - } - - pub async fn header(&self, hash: [u8; 32]) -> Result, SeraiError> { - self.call("chain_getHeader", [hex::encode(hash)]).await - } - - pub async fn block(&self, hash: [u8; 32]) -> Result, SeraiError> { - let block: Option = self.call("chain_getBlockBin", [hex::encode(hash)]).await?; - let Some(block) = block else { return Ok(None) }; - let Ok(bytes) = Self::hex_decode(block) else { - Err(SeraiError::InvalidNode("didn't return a hex-encoded block".to_string()))? - }; - let Ok(block) = Block::decode(&mut bytes.as_slice()) else { - Err(SeraiError::InvalidNode("didn't return a block".to_string()))? - }; - Ok(Some(block)) - } - - pub async fn latest_finalized_block(&self) -> Result { - let latest = self.latest_finalized_block_hash().await?; - let Some(block) = self.block(latest).await? else { - Err(SeraiError::InvalidNode("node didn't have a latest block".to_string()))? - }; - Ok(block) - } - - // There is no provided method for this - // TODO: Add one to Serai - pub async fn is_finalized(&self, header: &Header) -> Result { - // Get the latest finalized block - let finalized = self.latest_finalized_block_hash().await?; - // If the latest finalized block is this block, return true - if finalized == header.hash().as_ref() { - return Ok(true); - } - - let Some(finalized) = self.header(finalized).await? else { - Err(SeraiError::InvalidNode("couldn't get finalized header".to_string()))? - }; - - // If the finalized block has a lower number, this block can't be finalized - if finalized.number < header.number { - return Ok(false); - } - - // This block, if finalized, comes before the finalized block - // If we request the hash of this block's number, Substrate will return the hash on the main - // chain - // If that hash is this hash, this block is finalized - let Some(hash) = self.block_hash(header.number).await? else { - // This is an error since there is a finalized block at this index - Err(SeraiError::InvalidNode( - "couldn't get block hash for a block number below the finalized block".to_string(), - ))? - }; - - Ok(header.hash().as_ref() == hash) - } - - pub async fn finalized_block_by_number(&self, number: u64) -> Result, SeraiError> { - let hash = self.block_hash(number).await?; - let Some(hash) = hash else { return Ok(None) }; - let Some(block) = self.block(hash).await? else { return Ok(None) }; - if !self.is_finalized(&block.header).await? { - return Ok(None); - } - Ok(Some(block)) - } - - /* - /// A stream which yields whenever new block(s) have been finalized. - pub async fn newly_finalized_block( - &self, - ) -> Result>, SeraiError> { - Ok(self.0.rpc().subscribe_finalized_block_headers().await - .map_err(|_| SeraiError::ConnectionError)?.map( - |next| { - next.map_err(|_| SeraiError::ConnectionError)?; - Ok(()) - }, - )) - } - - pub async fn nonce(&self, address: &SeraiAddress) -> Result { - self - .0 - .rpc() - .system_account_next_index(&sp_core::sr25519::Public(address.0).to_string()) - .await - .map_err(|_| SeraiError::ConnectionError) - } - */ - - /// Create a TemporalSerai bound to whatever is currently the latest finalized block. - /// - /// The binding occurs at time of call. This does not track the latest finalized block and update - /// itself. - pub async fn as_of_latest_finalized_block(&self) -> Result, SeraiError> { - let latest = self.latest_finalized_block_hash().await?; - Ok(TemporalSerai { serai: self, block: latest, events: RwLock::new(None) }) - } - - /// Returns a TemporalSerai able to retrieve state as of the specified block. - pub fn as_of(&self, block: [u8; 32]) -> TemporalSerai<'_> { - TemporalSerai { serai: self, block, events: RwLock::new(None) } - } - - /// Return the P2P Multiaddrs for the validators of the specified network. - pub async fn p2p_validators( - &self, - network: ExternalNetworkId, - ) -> Result, SeraiError> { - self.call("p2p_validators", [network]).await - } - - // TODO: move this to SeraiValidatorSets? - pub async fn external_network_address( - &self, - network: ExternalNetworkId, - ) -> Result { - self.call("external_network_address", [network]).await - } - - // TODO: move this to SeraiInInstructions? - pub async fn encoded_shorthand(&self, shorthand: Shorthand) -> Result, SeraiError> { - self.call("encoded_shorthand", shorthand).await - } - - // TODO: move this to SeraiDex? - pub async fn quote_price(&self, params: QuotePriceParams) -> Result { - self.call("quote_price", params).await - } -} - -impl TemporalSerai<'_> { - async fn events( - &self, - filter_map: impl Fn(&Event) -> Option, - ) -> Result, SeraiError> { - let mut events = self.events.read().await; - if events.is_none() { - drop(events); - let mut events_write = self.events.write().await; - if events_write.is_none() { - *events_write = Some(self.storage("System", "Events", ()).await?.unwrap_or(vec![])); - } - drop(events_write); - events = self.events.read().await; - } - - let mut res = vec![]; - for event in events.as_ref().unwrap() { - if let Some(event) = filter_map(&event.event) { - res.push(event); - } - } - Ok(res) - } - - async fn storage( - &self, - pallet: &'static str, - name: &'static str, - key: K, - ) -> Result, SeraiError> { - // TODO: Make this const? - let mut full_key = sp_core::hashing::twox_128(pallet.as_bytes()).to_vec(); - full_key.extend(sp_core::hashing::twox_128(name.as_bytes())); - full_key.extend(key.encode()); - - let res: Option = - self.serai.call("state_getStorage", [hex::encode(full_key), hex::encode(self.block)]).await?; - let Some(res) = res else { return Ok(None) }; - let res = Serai::hex_decode(res)?; - Ok(Some(R::decode(&mut res.as_slice()).map_err(|_| { - SeraiError::InvalidRuntime(format!( - "different type present at storage location, raw value: {}", - hex::encode(res) - )) - })?)) - } - - async fn runtime_api( - &self, - method: &'static str, - params: P, - ) -> Result { - let result: String = self - .serai - .call( - "state_call", - [method.to_string(), hex::encode(params.encode()), hex::encode(self.block)], - ) - .await?; - - let bytes = Serai::hex_decode(result.clone())?; - R::decode(&mut bytes.as_slice()).map_err(|_| { - SeraiError::InvalidRuntime(format!( - "different type than what is expected to be returned, raw value: {}", - hex::encode(result) - )) - }) - } - - pub fn coins(&self) -> SeraiCoins<'_> { - SeraiCoins(self) - } - - pub fn dex(&self) -> SeraiDex<'_> { - SeraiDex(self) - } - - pub fn in_instructions(&self) -> SeraiInInstructions<'_> { - SeraiInInstructions(self) - } - - pub fn validator_sets(&self) -> SeraiValidatorSets<'_> { - SeraiValidatorSets(self) - } - - pub fn genesis_liquidity(&self) -> SeraiGenesisLiquidity<'_> { - SeraiGenesisLiquidity(self) - } - - pub fn liquidity_tokens(&self) -> SeraiLiquidityTokens<'_> { - SeraiLiquidityTokens(self) - } -} diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs deleted file mode 100644 index a978b494..00000000 --- a/substrate/client/src/serai/validator_sets.rs +++ /dev/null @@ -1,248 +0,0 @@ -use scale::Encode; - -use sp_core::sr25519::{Public, Signature}; -use sp_runtime::BoundedVec; - -use serai_abi::{primitives::Amount, validator_sets::primitives::ExternalValidatorSet}; -pub use serai_abi::validator_sets::primitives; -use primitives::{MAX_KEY_LEN, Session, KeyPair, SlashReport}; - -use crate::{ - primitives::{NetworkId, ExternalNetworkId, EmbeddedEllipticCurve}, - Transaction, Serai, TemporalSerai, SeraiError, -}; - -const PALLET: &str = "ValidatorSets"; - -pub type ValidatorSetsEvent = serai_abi::validator_sets::Event; - -#[derive(Clone, Copy)] -pub struct SeraiValidatorSets<'a>(pub(crate) &'a TemporalSerai<'a>); -impl SeraiValidatorSets<'_> { - pub async fn new_set_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::ValidatorSets(event) = event { - if matches!(event, ValidatorSetsEvent::NewSet { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub async fn participant_removed_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::ValidatorSets(event) = event { - if matches!(event, ValidatorSetsEvent::ParticipantRemoved { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub async fn key_gen_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::ValidatorSets(event) = event { - if matches!(event, ValidatorSetsEvent::KeyGen { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub async fn accepted_handover_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::ValidatorSets(event) = event { - if matches!(event, ValidatorSetsEvent::AcceptedHandover { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub async fn set_retired_events(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::ValidatorSets(event) = event { - if matches!(event, ValidatorSetsEvent::SetRetired { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - - pub async fn session(&self, network: NetworkId) -> Result, SeraiError> { - self.0.storage(PALLET, "CurrentSession", network).await - } - - pub async fn embedded_elliptic_curve_key( - &self, - validator: Public, - embedded_elliptic_curve: EmbeddedEllipticCurve, - ) -> Result>, SeraiError> { - self - .0 - .storage( - PALLET, - "EmbeddedEllipticCurveKeys", - (sp_core::hashing::blake2_128(&validator.encode()), validator, embedded_elliptic_curve), - ) - .await - } - - pub async fn participants( - &self, - network: NetworkId, - ) -> Result>, SeraiError> { - self.0.storage(PALLET, "Participants", network).await - } - - pub async fn allocation_per_key_share( - &self, - network: NetworkId, - ) -> Result, SeraiError> { - self.0.storage(PALLET, "AllocationPerKeyShare", network).await - } - - pub async fn total_allocated_stake( - &self, - network: NetworkId, - ) -> Result, SeraiError> { - self.0.storage(PALLET, "TotalAllocatedStake", network).await - } - - pub async fn allocation( - &self, - network: NetworkId, - key: Public, - ) -> Result, SeraiError> { - self - .0 - .storage( - PALLET, - "Allocations", - (sp_core::hashing::blake2_128(&(network, key).encode()), (network, key)), - ) - .await - } - - pub async fn pending_deallocations( - &self, - network: NetworkId, - account: Public, - session: Session, - ) -> Result, SeraiError> { - self - .0 - .storage( - PALLET, - "PendingDeallocations", - (sp_core::hashing::blake2_128(&(network, account).encode()), (network, account, session)), - ) - .await - } - - pub async fn active_network_validators( - &self, - network: NetworkId, - ) -> Result, SeraiError> { - self.0.runtime_api("ValidatorSetsApi_validators", network).await - } - - // TODO: Store these separately since we almost never need both at once? - pub async fn keys(&self, set: ExternalValidatorSet) -> Result, SeraiError> { - self.0.storage(PALLET, "Keys", (sp_core::hashing::twox_64(&set.encode()), set)).await - } - - pub async fn key_pending_slash_report( - &self, - network: ExternalNetworkId, - ) -> Result, SeraiError> { - self.0.storage(PALLET, "PendingSlashReport", network).await - } - - pub async fn session_begin_block( - &self, - network: NetworkId, - session: Session, - ) -> Result, SeraiError> { - self.0.storage(PALLET, "SessionBeginBlock", (network, session)).await - } - - pub fn set_keys( - network: ExternalNetworkId, - key_pair: KeyPair, - signature_participants: bitvec::vec::BitVec, - signature: Signature, - ) -> Transaction { - Serai::unsigned(serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::set_keys { - network, - key_pair, - signature_participants, - signature, - })) - } - - pub fn set_embedded_elliptic_curve_key( - embedded_elliptic_curve: EmbeddedEllipticCurve, - key: BoundedVec>, - ) -> serai_abi::Call { - serai_abi::Call::ValidatorSets( - serai_abi::validator_sets::Call::set_embedded_elliptic_curve_key { - embedded_elliptic_curve, - key, - }, - ) - } - - pub fn allocate(network: NetworkId, amount: Amount) -> serai_abi::Call { - serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::allocate { network, amount }) - } - - pub fn deallocate(network: NetworkId, amount: Amount) -> serai_abi::Call { - serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::deallocate { network, amount }) - } - - pub fn report_slashes( - network: ExternalNetworkId, - slashes: SlashReport, - signature: Signature, - ) -> Transaction { - Serai::unsigned(serai_abi::Call::ValidatorSets( - serai_abi::validator_sets::Call::report_slashes { network, slashes, signature }, - )) - } -} diff --git a/substrate/client/src/tests/mod.rs b/substrate/client/src/tests/mod.rs deleted file mode 100644 index 3ffa8630..00000000 --- a/substrate/client/src/tests/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "networks")] -mod networks; diff --git a/substrate/client/src/tests/networks/bitcoin.rs b/substrate/client/src/tests/networks/bitcoin.rs deleted file mode 100644 index 2e93d51b..00000000 --- a/substrate/client/src/tests/networks/bitcoin.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO: Test the address back and forth diff --git a/substrate/client/src/tests/networks/mod.rs b/substrate/client/src/tests/networks/mod.rs deleted file mode 100644 index 5dd6b762..00000000 --- a/substrate/client/src/tests/networks/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[cfg(feature = "bitcoin")] -mod bitcoin; - -#[cfg(feature = "monero")] -mod monero; diff --git a/substrate/client/src/tests/networks/monero.rs b/substrate/client/src/tests/networks/monero.rs deleted file mode 100644 index 2e93d51b..00000000 --- a/substrate/client/src/tests/networks/monero.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO: Test the address back and forth diff --git a/substrate/client/tests/batch.rs b/substrate/client/tests/batch.rs deleted file mode 100644 index 2d32462f..00000000 --- a/substrate/client/tests/batch.rs +++ /dev/null @@ -1,76 +0,0 @@ -use rand_core::{RngCore, OsRng}; - -use blake2::{ - digest::{consts::U32, Digest}, - Blake2b, -}; - -use scale::Encode; - -use serai_client::{ - primitives::{BlockHash, ExternalCoin, Amount, ExternalBalance, SeraiAddress}, - coins::CoinsEvent, - validator_sets::primitives::Session, - in_instructions::{ - primitives::{InInstruction, InInstructionWithBalance, Batch}, - InInstructionsEvent, - }, - Serai, -}; - -mod common; -use common::in_instructions::provide_batch; - -serai_test!( - publish_batch: (|serai: Serai| async move { - let id = 0; - - let mut address = SeraiAddress::new([0; 32]); - OsRng.fill_bytes(&mut address.0); - - let coin = ExternalCoin::Bitcoin; - let network = coin.network(); - let amount = Amount(OsRng.next_u64().saturating_add(1)); - let balance = ExternalBalance { coin, amount }; - - let mut external_network_block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut external_network_block_hash.0); - - let batch = Batch { - network, - id, - external_network_block_hash, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Transfer(address), - balance, - }], - }; - - let block = provide_batch(&serai, batch.clone()).await; - - let serai = serai.as_of(block); - { - let serai = serai.in_instructions(); - let batches = serai.batch_events().await.unwrap(); - assert_eq!( - batches, - vec![InInstructionsEvent::Batch { - network, - publishing_session: Session(0), - id, - external_network_block_hash, - in_instructions_hash: Blake2b::::digest(batch.instructions.encode()).into(), - in_instruction_results: bitvec::bitvec![u8, bitvec::order::Lsb0; 1; 1], - }] - ); - } - - let serai = serai.coins(); - assert_eq!( - serai.mint_events().await.unwrap(), - vec![CoinsEvent::Mint { to: address, balance: balance.into() }] - ); - assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), amount); - assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), amount); - }) -); diff --git a/substrate/client/tests/burn.rs b/substrate/client/tests/burn.rs deleted file mode 100644 index 8351781e..00000000 --- a/substrate/client/tests/burn.rs +++ /dev/null @@ -1,105 +0,0 @@ -use rand_core::{RngCore, OsRng}; - -use blake2::{ - digest::{consts::U32, Digest}, - Blake2b, -}; - -use scale::Encode; - -use sp_core::Pair; - -use serai_client::{ - primitives::{ - BlockHash, ExternalCoin, Amount, ExternalBalance, SeraiAddress, ExternalAddress, - insecure_pair_from_name, - }, - coins::{ - primitives::{OutInstruction, OutInstructionWithBalance}, - CoinsEvent, - }, - validator_sets::primitives::Session, - in_instructions::{ - InInstructionsEvent, - primitives::{InInstruction, InInstructionWithBalance, Batch}, - }, - Serai, SeraiCoins, -}; - -mod common; -use common::{tx::publish_tx, in_instructions::provide_batch}; - -serai_test!( - burn: (|serai: Serai| async move { - let id = 0; - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - - let pair = insecure_pair_from_name("Dave"); - let public = pair.public(); - let address = SeraiAddress::from(public); - - let coin = ExternalCoin::Bitcoin; - let network = coin.network(); - let amount = Amount(OsRng.next_u64().saturating_add(1)); - let balance = ExternalBalance { coin, amount }; - - let batch = Batch { - network, - id, - external_network_block_hash: block_hash, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Transfer(address), - balance, - }], - }; - - let block = provide_batch(&serai, batch.clone()).await; - - let instruction = { - let serai = serai.as_of(block); - let batches = serai.in_instructions().batch_events().await.unwrap(); - assert_eq!( - batches, - vec![InInstructionsEvent::Batch { - network, - publishing_session: Session(0), - id, - external_network_block_hash: block_hash, - in_instructions_hash: Blake2b::::digest(batch.instructions.encode()).into(), - in_instruction_results: bitvec::bitvec![u8, bitvec::order::Lsb0; 1; 1], - }] - ); - - assert_eq!( - serai.coins().mint_events().await.unwrap(), - vec![CoinsEvent::Mint { to: address, balance: balance.into() }] - ); - assert_eq!(serai.coins().coin_supply(coin.into()).await.unwrap(), amount); - assert_eq!(serai.coins().coin_balance(coin.into(), address).await.unwrap(), amount); - - // Now burn it - let mut rand_bytes = vec![0; 32]; - OsRng.fill_bytes(&mut rand_bytes); - let external_address = ExternalAddress::new(rand_bytes).unwrap(); - - OutInstructionWithBalance { - balance, - instruction: OutInstruction { address: external_address }, - } - }; - - let block = publish_tx( - &serai, - &serai.sign(&pair, SeraiCoins::burn_with_instruction(instruction.clone()), 0, 0), - ) - .await; - - let serai = serai.as_of(block); - let serai = serai.coins(); - let events = serai.burn_with_instruction_events().await.unwrap(); - assert_eq!(events, vec![CoinsEvent::BurnWithInstruction { from: address, instruction }]); - assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), Amount(0)); - assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), Amount(0)); - }) -); diff --git a/substrate/client/tests/common/dex.rs b/substrate/client/tests/common/dex.rs deleted file mode 100644 index a5ea2518..00000000 --- a/substrate/client/tests/common/dex.rs +++ /dev/null @@ -1,49 +0,0 @@ -use serai_abi::primitives::{Amount, Coin, ExternalCoin}; - -use serai_client::{Serai, SeraiDex}; -use sp_core::{sr25519::Pair, Pair as PairTrait}; - -use crate::common::tx::publish_tx; - -#[allow(dead_code)] -pub async fn add_liquidity( - serai: &Serai, - coin: ExternalCoin, - coin_amount: Amount, - sri_amount: Amount, - nonce: u32, - pair: Pair, -) -> [u8; 32] { - let address = pair.public(); - - let tx = serai.sign( - &pair, - SeraiDex::add_liquidity(coin, coin_amount, sri_amount, Amount(1), Amount(1), address.into()), - nonce, - 0, - ); - - publish_tx(serai, &tx).await -} - -#[allow(dead_code)] -pub async fn swap( - serai: &Serai, - from_coin: Coin, - to_coin: Coin, - amount_in: Amount, - amount_out_min: Amount, - nonce: u32, - pair: Pair, -) -> [u8; 32] { - let address = pair.public(); - - let tx = serai.sign( - &pair, - SeraiDex::swap(from_coin, to_coin, amount_in, amount_out_min, address.into()), - nonce, - Default::default(), - ); - - publish_tx(serai, &tx).await -} diff --git a/substrate/client/tests/common/genesis_liquidity.rs b/substrate/client/tests/common/genesis_liquidity.rs deleted file mode 100644 index 82bff745..00000000 --- a/substrate/client/tests/common/genesis_liquidity.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::HashMap; - -use rand_core::{RngCore, OsRng}; -use zeroize::Zeroizing; - -use frost::curve::Ristretto; -use ciphersuite::{WrappedGroup, GroupIo}; -use dkg_musig::musig; -use schnorrkel::Schnorrkel; - -use sp_core::{sr25519::Signature, Pair as PairTrait}; - -use serai_abi::{ - primitives::{ - EXTERNAL_COINS, BlockHash, ExternalNetworkId, NetworkId, ExternalCoin, Amount, ExternalBalance, - SeraiAddress, insecure_pair_from_name, - }, - validator_sets::primitives::{Session, ValidatorSet, musig_context}, - genesis_liquidity::primitives::{Values, oraclize_values_message}, - in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, -}; - -use serai_client::{Serai, SeraiGenesisLiquidity}; - -use crate::common::{in_instructions::provide_batch, tx::publish_tx}; - -#[allow(dead_code)] -pub async fn set_up_genesis( - serai: &Serai, - values: &HashMap, -) -> (HashMap>, HashMap) { - // make accounts with amounts - let mut accounts = HashMap::new(); - for coin in EXTERNAL_COINS { - // make 5 accounts per coin - let mut values = vec![]; - for _ in 0 .. 5 { - let mut address = SeraiAddress::new([0; 32]); - OsRng.fill_bytes(&mut address.0); - values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals())))); - } - accounts.insert(coin, values); - } - - // send a batch per coin - let mut batch_ids: HashMap = HashMap::new(); - for coin in EXTERNAL_COINS { - // set up instructions - let instructions = accounts[&coin] - .iter() - .map(|(addr, amount)| InInstructionWithBalance { - instruction: InInstruction::GenesisLiquidity(*addr), - balance: ExternalBalance { coin, amount: *amount }, - }) - .collect::>(); - - // set up block hash - let mut block = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block.0); - - // set up batch id - batch_ids - .entry(coin.network()) - .and_modify(|v| { - *v += 1; - }) - .or_insert(0); - - let batch = Batch { - network: coin.network(), - external_network_block_hash: block, - id: batch_ids[&coin.network()], - instructions, - }; - provide_batch(serai, batch).await; - } - - // set values relative to each other. We can do that without checking for genesis period blocks - // since we are running in test(fast-epoch) mode. - // TODO: Random values here - let values = Values { - monero: values[&ExternalCoin::Monero], - ether: values[&ExternalCoin::Ether], - dai: values[&ExternalCoin::Dai], - }; - set_values(serai, &values).await; - - (accounts, batch_ids) -} - -#[allow(dead_code)] -async fn set_values(serai: &Serai, values: &Values) { - // prepare a Musig tx to oraclize the relative values - let pair = insecure_pair_from_name("Alice"); - let public = pair.public(); - // we publish the tx in set 1 - let set = ValidatorSet { session: Session(1), network: NetworkId::Serai }; - - let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); - let secret_key = - ::read_F::<&[u8]>(&mut pair.as_ref().secret.to_bytes()[.. 32].as_ref()) - .unwrap(); - - assert_eq!(Ristretto::generator() * secret_key, public_key); - let threshold_keys = - musig::(musig_context(set), Zeroizing::new(secret_key), &[public_key]).unwrap(); - - let sig = frost::tests::sign_without_caching( - &mut OsRng, - frost::tests::algorithm_machines( - &mut OsRng, - &Schnorrkel::new(b"substrate"), - &HashMap::from([(threshold_keys.params().i(), threshold_keys)]), - ), - &oraclize_values_message(&set, values), - ); - - // oraclize values - let _ = - publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes()))) - .await; -} diff --git a/substrate/client/tests/common/in_instructions.rs b/substrate/client/tests/common/in_instructions.rs deleted file mode 100644 index 87e26c5d..00000000 --- a/substrate/client/tests/common/in_instructions.rs +++ /dev/null @@ -1,98 +0,0 @@ -use rand_core::{RngCore, OsRng}; -use blake2::{ - digest::{consts::U32, Digest}, - Blake2b, -}; - -use scale::Encode; - -use sp_core::Pair; - -use serai_client::{ - primitives::{BlockHash, ExternalBalance, SeraiAddress, insecure_pair_from_name}, - validator_sets::primitives::{ExternalValidatorSet, KeyPair}, - in_instructions::{ - primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance}, - InInstructionsEvent, - }, - SeraiInInstructions, Serai, -}; - -use crate::common::{tx::publish_tx, validator_sets::set_keys}; - -#[allow(dead_code)] -pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] { - let serai_latest = serai.as_of_latest_finalized_block().await.unwrap(); - let session = serai_latest.validator_sets().session(batch.network.into()).await.unwrap().unwrap(); - let set = ExternalValidatorSet { session, network: batch.network }; - - let pair = insecure_pair_from_name(&format!("ValidatorSet {set:?}")); - let keys = if let Some(keys) = serai_latest.validator_sets().keys(set).await.unwrap() { - keys - } else { - let keys = KeyPair(pair.public(), vec![].try_into().unwrap()); - set_keys(serai, set, keys.clone(), &[insecure_pair_from_name("Alice")]).await; - keys - }; - assert_eq!(keys.0, pair.public()); - - let block = publish_tx( - serai, - &SeraiInInstructions::execute_batch(SignedBatch { - batch: batch.clone(), - signature: pair.sign(&batch_message(&batch)), - }), - ) - .await; - - { - let mut batches = serai.as_of(block).in_instructions().batch_events().await.unwrap(); - assert_eq!(batches.len(), 1); - let InInstructionsEvent::Batch { - network, - publishing_session, - id, - external_network_block_hash, - in_instructions_hash, - in_instruction_results: _, - } = batches.swap_remove(0) - else { - panic!("Batch event wasn't Batch event") - }; - assert_eq!(network, batch.network); - assert_eq!(publishing_session, session); - assert_eq!(id, batch.id); - assert_eq!(external_network_block_hash, batch.external_network_block_hash); - assert_eq!( - in_instructions_hash, - <[u8; 32]>::from(Blake2b::::digest(batch.instructions.encode())) - ); - } - - // TODO: Check the tokens events - - block -} - -#[allow(dead_code)] -pub async fn mint_coin( - serai: &Serai, - balance: ExternalBalance, - batch_id: u32, - address: SeraiAddress, -) -> [u8; 32] { - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - - let batch = Batch { - network: balance.coin.network(), - id: batch_id, - external_network_block_hash: block_hash, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Transfer(address), - balance, - }], - }; - - provide_batch(serai, batch).await -} diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs deleted file mode 100644 index 560eb8ab..00000000 --- a/substrate/client/tests/common/mod.rs +++ /dev/null @@ -1,140 +0,0 @@ -pub mod tx; -pub mod validator_sets; -pub mod in_instructions; -pub mod dex; -pub mod genesis_liquidity; - -#[macro_export] -macro_rules! serai_test { - ($($name: ident: $test: expr)*) => { - $( - #[tokio::test] - async fn $name() { - use std::collections::HashMap; - use dockertest::{ - PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, - TestBodySpecification, DockerTest, - }; - - serai_docker_tests::build("serai".to_string()); - - let handle = concat!("serai_client-serai_node-", stringify!($name)); - - let composition = TestBodySpecification::with_image( - Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), - ) - .replace_cmd(vec![ - "serai-node".to_string(), - "--dev".to_string(), - "--unsafe-rpc-external".to_string(), - "--rpc-cors".to_string(), - "all".to_string(), - ]) - .replace_env( - HashMap::from([ - ("RUST_LOG".to_string(), "runtime=debug".to_string()), - ("KEY".to_string(), " ".to_string()), - ]) - ) - .set_publish_all_ports(true) - .set_handle(handle) - .set_start_policy(StartPolicy::Strict) - .set_log_options(Some(LogOptions { - action: LogAction::Forward, - policy: LogPolicy::Always, - source: LogSource::Both, - })); - - let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); - test.provide_container(composition); - test.run_async(|ops| async move { - // Sleep until the Substrate RPC starts - let mut ticks = 0; - let serai_rpc = loop { - // Bound execution to 60 seconds - if ticks > 60 { - panic!("Serai node didn't start within 60 seconds"); - } - tokio::time::sleep(core::time::Duration::from_secs(1)).await; - ticks += 1; - - let Some(serai_rpc) = ops.handle(handle).host_port(9944) else { continue }; - let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1); - - let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue }; - if client.latest_finalized_block_hash().await.is_err() { - continue; - } - break serai_rpc; - }; - #[allow(clippy::redundant_closure_call)] - $test(Serai::new(serai_rpc).await.unwrap()).await; - }).await; - } - )* - } -} - -#[macro_export] -macro_rules! serai_test_fast_epoch { - ($($name: ident: $test: expr)*) => { - $( - #[tokio::test] - async fn $name() { - use std::collections::HashMap; - use dockertest::{ - PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, - TestBodySpecification, DockerTest, - }; - - serai_docker_tests::build("serai-fast-epoch".to_string()); - - let handle = concat!("serai_client-serai_node-", stringify!($name)); - - let composition = TestBodySpecification::with_image( - Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never), - ) - .replace_cmd(vec![ - "serai-node".to_string(), - "--dev".to_string(), - "--unsafe-rpc-external".to_string(), - "--rpc-cors".to_string(), - "all".to_string(), - ]) - .replace_env( - HashMap::from([ - ("RUST_LOG".to_string(), "runtime=debug".to_string()), - ("KEY".to_string(), " ".to_string()), - ]) - ) - .set_publish_all_ports(true) - .set_handle(handle) - .set_start_policy(StartPolicy::Strict) - .set_log_options(Some(LogOptions { - action: LogAction::Forward, - policy: LogPolicy::Always, - source: LogSource::Both, - })); - - let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); - test.provide_container(composition); - test.run_async(|ops| async move { - // Sleep until the Substrate RPC starts - let serai_rpc = ops.handle(handle).host_port(9944).unwrap(); - let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1); - // Bound execution to 60 seconds - for _ in 0 .. 60 { - tokio::time::sleep(core::time::Duration::from_secs(1)).await; - let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue }; - if client.latest_finalized_block_hash().await.is_err() { - continue; - } - break; - } - #[allow(clippy::redundant_closure_call)] - $test(Serai::new(serai_rpc).await.unwrap()).await; - }).await; - } - )* - } -} diff --git a/substrate/client/tests/common/tx.rs b/substrate/client/tests/common/tx.rs deleted file mode 100644 index 768499b2..00000000 --- a/substrate/client/tests/common/tx.rs +++ /dev/null @@ -1,46 +0,0 @@ -use core::time::Duration; - -use tokio::time::sleep; - -use serai_client::{Transaction, Serai}; - -#[allow(dead_code)] -pub async fn publish_tx(serai: &Serai, tx: &Transaction) -> [u8; 32] { - let mut latest = serai - .block(serai.latest_finalized_block_hash().await.unwrap()) - .await - .unwrap() - .unwrap() - .number(); - - serai.publish(tx).await.unwrap(); - - // Get the block it was included in - // TODO: Add an RPC method for this/check the guarantee on the subscription - let mut ticks = 0; - loop { - latest += 1; - - let block = { - let mut block; - while { - block = serai.finalized_block_by_number(latest).await.unwrap(); - block.is_none() - } { - sleep(Duration::from_secs(1)).await; - ticks += 1; - - if ticks > 60 { - panic!("60 seconds without inclusion in a finalized block"); - } - } - block.unwrap() - }; - - for transaction in &block.transactions { - if transaction == tx { - return block.hash(); - } - } - } -} diff --git a/substrate/client/tests/common/validator_sets.rs b/substrate/client/tests/common/validator_sets.rs deleted file mode 100644 index f228c10d..00000000 --- a/substrate/client/tests/common/validator_sets.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::collections::HashMap; - -use zeroize::Zeroizing; -use rand_core::OsRng; - -use frost::curve::Ristretto; -use ciphersuite::{WrappedGroup, GroupIo}; -use dkg_musig::musig; -use schnorrkel::Schnorrkel; - -use sp_core::{ - ConstU32, - bounded::BoundedVec, - sr25519::{Pair, Signature}, - Pair as PairTrait, -}; - -use serai_abi::primitives::NetworkId; - -use serai_client::{ - primitives::{EmbeddedEllipticCurve, Amount}, - validator_sets::{ - primitives::{MAX_KEY_LEN, ExternalValidatorSet, KeyPair, musig_context, set_keys_message}, - ValidatorSetsEvent, - }, - SeraiValidatorSets, Serai, -}; - -use crate::common::tx::publish_tx; - -#[allow(dead_code)] -pub async fn set_keys( - serai: &Serai, - set: ExternalValidatorSet, - key_pair: KeyPair, - pairs: &[Pair], -) -> [u8; 32] { - let mut pub_keys = vec![]; - for pair in pairs { - let public_key = - ::read_G::<&[u8]>(&mut pair.public().0.as_ref()).unwrap(); - pub_keys.push(public_key); - } - - let mut threshold_keys = vec![]; - for i in 0 .. pairs.len() { - let secret_key = ::read_F::<&[u8]>( - &mut pairs[i].as_ref().secret.to_bytes()[.. 32].as_ref(), - ) - .unwrap(); - assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]); - - threshold_keys.push( - musig::(musig_context(set.into()), Zeroizing::new(secret_key), &pub_keys).unwrap(), - ); - } - - let mut musig_keys = HashMap::new(); - for threshold_keys in threshold_keys { - musig_keys.insert(threshold_keys.params().i(), threshold_keys); - } - - let sig = frost::tests::sign_without_caching( - &mut OsRng, - frost::tests::algorithm_machines(&mut OsRng, &Schnorrkel::new(b"substrate"), &musig_keys), - &set_keys_message(&set, &key_pair), - ); - - // Set the key pair - let block = publish_tx( - serai, - &SeraiValidatorSets::set_keys( - set.network, - key_pair.clone(), - bitvec::bitvec!(u8, bitvec::prelude::Lsb0; 1; musig_keys.len()), - Signature(sig.to_bytes()), - ), - ) - .await; - - assert_eq!( - serai.as_of(block).validator_sets().key_gen_events().await.unwrap(), - vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] - ); - assert_eq!(serai.as_of(block).validator_sets().keys(set).await.unwrap(), Some(key_pair)); - - block -} - -#[allow(dead_code)] -pub async fn set_embedded_elliptic_curve_key( - serai: &Serai, - pair: &Pair, - embedded_elliptic_curve: EmbeddedEllipticCurve, - key: BoundedVec>, - nonce: u32, -) -> [u8; 32] { - // get the call - let tx = serai.sign( - pair, - SeraiValidatorSets::set_embedded_elliptic_curve_key(embedded_elliptic_curve, key), - nonce, - 0, - ); - publish_tx(serai, &tx).await -} - -#[allow(dead_code)] -pub async fn allocate_stake( - serai: &Serai, - network: NetworkId, - amount: Amount, - pair: &Pair, - nonce: u32, -) -> [u8; 32] { - // get the call - let tx = serai.sign(pair, SeraiValidatorSets::allocate(network, amount), nonce, 0); - publish_tx(serai, &tx).await -} - -#[allow(dead_code)] -pub async fn deallocate_stake( - serai: &Serai, - network: NetworkId, - amount: Amount, - pair: &Pair, - nonce: u32, -) -> [u8; 32] { - // get the call - let tx = serai.sign(pair, SeraiValidatorSets::deallocate(network, amount), nonce, 0); - publish_tx(serai, &tx).await -} diff --git a/substrate/client/tests/dex.rs b/substrate/client/tests/dex.rs deleted file mode 100644 index e48512dc..00000000 --- a/substrate/client/tests/dex.rs +++ /dev/null @@ -1,434 +0,0 @@ -use rand_core::{RngCore, OsRng}; - -use sp_core::{Pair as PairTrait, bounded::BoundedVec}; - -use serai_abi::in_instructions::primitives::DexCall; - -use serai_client::{ - primitives::{ - BlockHash, ExternalCoin, Coin, Amount, ExternalBalance, Balance, SeraiAddress, ExternalAddress, - insecure_pair_from_name, - }, - in_instructions::primitives::{ - InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress, - }, - dex::DexEvent, - Serai, -}; - -mod common; -use common::{ - in_instructions::{provide_batch, mint_coin}, - dex::{add_liquidity as common_add_liquidity, swap as common_swap}, -}; - -// TODO: Calculate all constants in the following tests -// TODO: Check LP token, coin balances -// TODO: Modularize common code -// TODO: Check Transfer events -serai_test!( - add_liquidity: (|serai: Serai| async move { - let coin = ExternalCoin::Monero; - let pair = insecure_pair_from_name("Ferdie"); - - // mint sriXMR in the account so that we can add liq. - // Ferdie account is already pre-funded with SRI. - mint_coin( - &serai, - ExternalBalance { coin, amount: Amount(100_000_000_000_000) }, - 0, - pair.clone().public().into(), - ) - .await; - - // add liquidity - let coin_amount = Amount(50_000_000_000_000); - let sri_amount = Amount(50_000_000_000_000); - let block = common_add_liquidity(&serai, - coin, - coin_amount, - sri_amount, - 0, - pair.clone() - ).await; - // get only the add liq events - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. })); - - assert_eq!( - events, - vec![DexEvent::LiquidityAdded { - who: pair.public().into(), - mint_to: pair.public().into(), - pool_id: coin, - coin_amount: coin_amount.0, - sri_amount: sri_amount.0, - lp_token_minted: 49_999999990000 - }] - ); - }) - - // Tests coin -> SRI and SRI -> coin swaps. - swap_coin_to_sri: (|serai: Serai| async move { - let coin = ExternalCoin::Ether; - let pair = insecure_pair_from_name("Ferdie"); - - // mint sriXMR in the account so that we can add liq. - // Ferdie account is already pre-funded with SRI. - mint_coin( - &serai, - ExternalBalance { coin, amount: Amount(100_000_000_000_000) }, - 0, - pair.clone().public().into(), - ) - .await; - - // add liquidity - common_add_liquidity(&serai, - coin, - Amount(50_000_000_000_000), - Amount(50_000_000_000_000), - 0, - pair.clone() - ).await; - - // now that we have our liquid pool, swap some coin to SRI. - let mut amount_in = Amount(25_000_000_000_000); - let mut block = common_swap( - &serai, - coin.into(), - Coin::Serai, - amount_in, - Amount(1), - 1, - pair.clone()) - .await; - - // get only the swap events - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. })); - - let mut path = BoundedVec::try_from(vec![coin.into(), Coin::Serai]).unwrap(); - assert_eq!( - events, - vec![DexEvent::SwapExecuted { - who: pair.clone().public().into(), - send_to: pair.public().into(), - path, - amount_in: amount_in.0, - amount_out: 16633299966633 - }] - ); - - // now swap some SRI to coin - amount_in = Amount(10_000_000_000_000); - block = common_swap( - &serai, - Coin::Serai, - coin.into(), - amount_in, - Amount(1), - 2, - pair.clone() - ).await; - - // get only the swap events - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. })); - - path = BoundedVec::try_from(vec![Coin::Serai, coin.into()]).unwrap(); - assert_eq!( - events, - vec![DexEvent::SwapExecuted { - who: pair.clone().public().into(), - send_to: pair.public().into(), - path, - amount_in: amount_in.0, - amount_out: 17254428681101 - }] - ); - }) - - swap_coin_to_coin: (|serai: Serai| async move { - let coin1 = ExternalCoin::Monero; - let coin2 = ExternalCoin::Dai; - let pair = insecure_pair_from_name("Ferdie"); - - // mint coins - mint_coin( - &serai, - ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) }, - 0, - pair.clone().public().into(), - ) - .await; - mint_coin( - &serai, - ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) }, - 0, - pair.clone().public().into(), - ) - .await; - - // add liquidity to pools - common_add_liquidity(&serai, - coin1, - Amount(50_000_000_000_000), - Amount(50_000_000_000_000), - 0, - pair.clone() - ).await; - common_add_liquidity(&serai, - coin2, - Amount(50_000_000_000_000), - Amount(50_000_000_000_000), - 1, - pair.clone() - ).await; - - // swap coin1 -> coin2 - let amount_in = Amount(25_000_000_000_000); - let block = common_swap( - &serai, - coin1.into(), - coin2.into(), - amount_in, - Amount(1), - 2, - pair.clone() - ).await; - - // get only the swap events - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. })); - - let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap(); - assert_eq!( - events, - vec![DexEvent::SwapExecuted { - who: pair.clone().public().into(), - send_to: pair.public().into(), - path, - amount_in: amount_in.0, - amount_out: 12453103964435, - }] - ); - }) - - add_liquidity_in_instructions: (|serai: Serai| async move { - let coin = ExternalCoin::Bitcoin; - let pair = insecure_pair_from_name("Ferdie"); - let mut batch_id = 0; - - // mint sriBTC in the account so that we can add liq. - // Ferdie account is already pre-funded with SRI. - mint_coin( - &serai, - ExternalBalance { coin, amount: Amount(100_000_000_000_000) }, - batch_id, - pair.clone().public().into(), - ) - .await; - batch_id += 1; - - // add liquidity - common_add_liquidity(&serai, - coin, - Amount(5_000_000_000_000), - Amount(500_000_000_000), - 0, - pair.clone() - ).await; - - // now that we have our liquid SRI/BTC pool, we can add more liquidity to it via an - // InInstruction - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - let batch = Batch { - network: coin.network(), - id: batch_id, - external_network_block_hash: block_hash, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(pair.public().into())), - balance: ExternalBalance { coin, amount: Amount(20_000_000_000_000) }, - }], - }; - - let block = provide_batch(&serai, batch).await; - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. })); - assert_eq!( - events, - vec![DexEvent::LiquidityAdded { - who: IN_INSTRUCTION_EXECUTOR, - mint_to: pair.public().into(), - pool_id: coin, - coin_amount: 10_000_000_000_000, // half of sent amount - sri_amount: 111_333_778_668, - lp_token_minted: 1_054_092_553_383 - }] - ); - }) - - swap_in_instructions: (|serai: Serai| async move { - let coin1 = ExternalCoin::Monero; - let coin2 = ExternalCoin::Ether; - let pair = insecure_pair_from_name("Ferdie"); - let mut coin1_batch_id = 0; - let mut coin2_batch_id = 0; - - // mint coins - mint_coin( - &serai, - ExternalBalance { coin: coin1, amount: Amount(10_000_000_000_000_000) }, - coin1_batch_id, - pair.clone().public().into(), - ) - .await; - coin1_batch_id += 1; - mint_coin( - &serai, - ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) }, - coin2_batch_id, - pair.clone().public().into(), - ) - .await; - coin2_batch_id += 1; - - // add liquidity to pools - common_add_liquidity(&serai, - coin1, - Amount(5_000_000_000_000_000), // monero has 12 decimals - Amount(50_000_000_000), - 0, - pair.clone() - ).await; - common_add_liquidity(&serai, - coin2, - Amount(5_000_000_000_000), // ether still has 8 in our codebase - Amount(500_000_000_000), - 1, - pair.clone() - ).await; - - // rand address bytes - let mut rand_bytes = vec![0; 32]; - OsRng.fill_bytes(&mut rand_bytes); - - // XMR -> ETH - { - // make an out address - let out_address = OutAddress::External(ExternalAddress::new(rand_bytes.clone()).unwrap()); - - // amount is the min out amount - let out_balance = Balance { coin: coin2.into(), amount: Amount(1) }; - - // now that we have our pools, we can try to swap - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - let batch = Batch { - network: coin1.network(), - id: coin1_batch_id, - external_network_block_hash: block_hash, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address)), - balance: ExternalBalance { coin: coin1, amount: Amount(200_000_000_000_000) }, - }], - }; - - let block = provide_batch(&serai, batch).await; - coin1_batch_id += 1; - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. })); - - let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap(); - assert_eq!( - events, - vec![DexEvent::SwapExecuted { - who: IN_INSTRUCTION_EXECUTOR, - send_to: IN_INSTRUCTION_EXECUTOR, - path, - amount_in: 200_000_000_000_000, - amount_out: 19_044_944_233 - }] - ); - } - - // ETH -> sriXMR - { - // make an out address - let out_address = - OutAddress::Serai(SeraiAddress::new(rand_bytes.clone().try_into().unwrap())); - - // amount is the min out amount - let out_balance = Balance { coin: coin1.into(), amount: Amount(1) }; - - // now that we have our pools, we can try to swap - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - let batch = Batch { - network: coin2.network(), - id: coin2_batch_id, - external_network_block_hash: block_hash, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())), - balance: ExternalBalance { coin: coin2, amount: Amount(200_000_000_000) }, - }], - }; - - let block = provide_batch(&serai, batch).await; - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. })); - - let path = BoundedVec::try_from(vec![coin2.into(), Coin::Serai, coin1.into()]).unwrap(); - assert_eq!( - events, - vec![DexEvent::SwapExecuted { - who: IN_INSTRUCTION_EXECUTOR, - send_to: out_address.as_native().unwrap(), - path, - amount_in: 200_000_000_000, - amount_out: 1487294253782353 - }] - ); - } - - // XMR -> SRI - { - // make an out address - let out_address = OutAddress::Serai(SeraiAddress::new(rand_bytes.try_into().unwrap())); - - // amount is the min out amount - let out_balance = Balance { coin: Coin::Serai, amount: Amount(1) }; - - // now that we have our pools, we can try to swap - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - let batch = Batch { - network: coin1.network(), - id: coin1_batch_id, - external_network_block_hash: block_hash, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())), - balance: ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) }, - }], - }; - - let block = provide_batch(&serai, batch).await; - let mut events = serai.as_of(block).dex().events().await.unwrap(); - events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. })); - - let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai]).unwrap(); - assert_eq!( - events, - vec![DexEvent::SwapExecuted { - who: IN_INSTRUCTION_EXECUTOR, - send_to: out_address.as_native().unwrap(), - path, - amount_in: 100_000_000_000_000, - amount_out: 1_762_662_819 - }] - ); - } - }) -); diff --git a/substrate/client/tests/dht.rs b/substrate/client/tests/dht.rs deleted file mode 100644 index 8b8a078b..00000000 --- a/substrate/client/tests/dht.rs +++ /dev/null @@ -1,53 +0,0 @@ -use serai_client::{primitives::ExternalNetworkId, Serai}; - -#[tokio::test] -async fn dht() { - use dockertest::{ - PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, - TestBodySpecification, DockerTest, - }; - - serai_docker_tests::build("serai".to_string()); - - let handle = |name: &str| format!("serai_client-serai_node-{name}"); - let composition = |name: &str| { - TestBodySpecification::with_image( - Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), - ) - .replace_env( - [("SERAI_NAME".to_string(), name.to_string()), ("KEY".to_string(), " ".to_string())].into(), - ) - .set_publish_all_ports(true) - .set_handle(handle(name)) - .set_start_policy(StartPolicy::Strict) - .set_log_options(Some(LogOptions { - action: LogAction::Forward, - policy: LogPolicy::Always, - source: LogSource::Both, - })) - }; - - let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); - test.provide_container(composition("alice")); - test.provide_container(composition("bob")); - test.provide_container(composition("charlie")); - test.provide_container(composition("dave")); - test - .run_async(|ops| async move { - // Sleep until the Substrate RPC starts - let alice = handle("alice"); - let serai_rpc = ops.handle(&alice).host_port(9944).unwrap(); - let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1); - // Sleep for a minute - tokio::time::sleep(core::time::Duration::from_secs(60)).await; - // Check the DHT has been populated - assert!(!Serai::new(serai_rpc.clone()) - .await - .unwrap() - .p2p_validators(ExternalNetworkId::Bitcoin) - .await - .unwrap() - .is_empty()); - }) - .await; -} diff --git a/substrate/client/tests/emissions.rs b/substrate/client/tests/emissions.rs deleted file mode 100644 index 7ee843cb..00000000 --- a/substrate/client/tests/emissions.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::{time::Duration, collections::HashMap}; -use rand_core::{RngCore, OsRng}; - -use serai_client::TemporalSerai; - -use serai_abi::{ - primitives::{ - EXTERNAL_NETWORKS, NETWORKS, TARGET_BLOCK_TIME, FAST_EPOCH_DURATION, FAST_EPOCH_INITIAL_PERIOD, - BlockHash, ExternalNetworkId, NetworkId, ExternalCoin, Amount, ExternalBalance, - }, - validator_sets::primitives::Session, - emissions::primitives::{INITIAL_REWARD_PER_BLOCK, SECURE_BY}, - in_instructions::primitives::Batch, -}; - -use serai_client::Serai; - -mod common; -use common::{genesis_liquidity::set_up_genesis, in_instructions::provide_batch}; - -serai_test_fast_epoch!( - emissions: (|serai: Serai| async move { - test_emissions(serai).await; - }) -); - -async fn send_batches(serai: &Serai, ids: &mut HashMap) { - for network in EXTERNAL_NETWORKS { - // set up batch id - ids - .entry(network) - .and_modify(|v| { - *v += 1; - }) - .or_insert(0); - - // set up block hash - let mut block = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block.0); - - provide_batch( - serai, - Batch { - network, - id: ids[&network], - external_network_block_hash: block, - instructions: vec![], - }, - ) - .await; - } -} - -async fn test_emissions(serai: Serai) { - // set up the genesis - let values = HashMap::from([ - (ExternalCoin::Monero, 184100), - (ExternalCoin::Ether, 4785000), - (ExternalCoin::Dai, 1500), - ]); - let (_, mut batch_ids) = set_up_genesis(&serai, &values).await; - - // wait until genesis is complete - let mut genesis_complete_block = None; - while genesis_complete_block.is_none() { - tokio::time::sleep(Duration::from_secs(1)).await; - genesis_complete_block = serai - .as_of_latest_finalized_block() - .await - .unwrap() - .genesis_liquidity() - .genesis_complete_block() - .await - .unwrap(); - } - - for _ in 0 .. 3 { - // get current stakes - let mut current_stake = HashMap::new(); - for n in NETWORKS { - // TODO: investigate why serai network TAS isn't visible at session 0. - let stake = serai - .as_of_latest_finalized_block() - .await - .unwrap() - .validator_sets() - .total_allocated_stake(n) - .await - .unwrap() - .unwrap_or(Amount(0)) - .0; - current_stake.insert(n, stake); - } - - // wait for a session change - let current_session = wait_for_session_change(&serai).await; - - // get last block - let last_block = serai.latest_finalized_block().await.unwrap(); - let serai_latest = serai.as_of(last_block.hash()); - let change_block_number = last_block.number(); - - // get distances to ec security & block count of the previous session - let (distances, total_distance) = get_distances(&serai_latest, ¤t_stake).await; - let block_count = get_session_blocks(&serai_latest, current_session - 1).await; - - // calculate how much reward in this session - let reward_this_epoch = - if change_block_number < (genesis_complete_block.unwrap() + FAST_EPOCH_INITIAL_PERIOD) { - block_count * INITIAL_REWARD_PER_BLOCK - } else { - let blocks_until = SECURE_BY - change_block_number; - let block_reward = total_distance / blocks_until; - block_count * block_reward - }; - - let reward_per_network = distances - .into_iter() - .map(|(n, distance)| { - let reward = u64::try_from( - u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) / - u128::from(total_distance), - ) - .unwrap(); - (n, reward) - }) - .collect::>(); - - // retire the prev-set so that TotalAllocatedStake updated. - send_batches(&serai, &mut batch_ids).await; - - for (n, reward) in reward_per_network { - let stake = serai - .as_of_latest_finalized_block() - .await - .unwrap() - .validator_sets() - .total_allocated_stake(n) - .await - .unwrap() - .unwrap_or(Amount(0)) - .0; - - // all reward should automatically staked for the network since we are in initial period. - assert_eq!(stake, *current_stake.get(&n).unwrap() + reward); - } - - // TODO: check stake per address? - // TODO: check post ec security era - } -} - -/// Returns the required stake in terms SRI for a given `Balance`. -async fn required_stake(serai: &TemporalSerai<'_>, balance: ExternalBalance) -> u64 { - // This is inclusive to an increase in accuracy - let sri_per_coin = serai.dex().oracle_value(balance.coin).await.unwrap().unwrap_or(Amount(0)); - - // See dex-pallet for the reasoning on these - let coin_decimals = balance.coin.decimals().max(5); - let accuracy_increase = u128::from(10u64.pow(coin_decimals)); - - let total_coin_value = - u64::try_from(u128::from(balance.amount.0) * u128::from(sri_per_coin.0) / accuracy_increase) - .unwrap_or(u64::MAX); - - // required stake formula (COIN_VALUE * 1.5) + margin(20%) - let required_stake = total_coin_value.saturating_mul(3).saturating_div(2); - required_stake.saturating_add(total_coin_value.saturating_div(5)) -} - -async fn wait_for_session_change(serai: &Serai) -> u32 { - let current_session = serai - .as_of_latest_finalized_block() - .await - .unwrap() - .validator_sets() - .session(NetworkId::Serai) - .await - .unwrap() - .unwrap() - .0; - let next_session = current_session + 1; - - // lets wait double the epoch time. - tokio::time::timeout( - tokio::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2), - async { - while serai - .as_of_latest_finalized_block() - .await - .unwrap() - .validator_sets() - .session(NetworkId::Serai) - .await - .unwrap() - .unwrap() - .0 < - next_session - { - tokio::time::sleep(Duration::from_secs(6)).await; - } - }, - ) - .await - .unwrap(); - - next_session -} - -async fn get_distances( - serai: &TemporalSerai<'_>, - current_stake: &HashMap, -) -> (HashMap, u64) { - // we should be in the initial period, so calculate how much each network supposedly get.. - // we can check the supply to see how much coin hence liability we have. - let mut distances: HashMap = HashMap::new(); - let mut total_distance = 0; - for n in EXTERNAL_NETWORKS { - let mut required = 0; - for c in n.coins() { - let amount = serai.coins().coin_supply(c.into()).await.unwrap(); - required += required_stake(serai, ExternalBalance { coin: c, amount }).await; - } - - let mut current = *current_stake.get(&n.into()).unwrap(); - if current > required { - current = required; - } - - let distance = required - current; - total_distance += distance; - - distances.insert(n.into(), distance); - } - - // add serai network portion(20%) - let new_total_distance = total_distance.saturating_mul(10) / 8; - distances.insert(NetworkId::Serai, new_total_distance - total_distance); - total_distance = new_total_distance; - - (distances, total_distance) -} - -async fn get_session_blocks(serai: &TemporalSerai<'_>, session: u32) -> u64 { - let begin_block = serai - .validator_sets() - .session_begin_block(NetworkId::Serai, Session(session)) - .await - .unwrap() - .unwrap(); - - let next_begin_block = serai - .validator_sets() - .session_begin_block(NetworkId::Serai, Session(session + 1)) - .await - .unwrap() - .unwrap(); - - next_begin_block.saturating_sub(begin_block) -} diff --git a/substrate/client/tests/genesis_liquidity.rs b/substrate/client/tests/genesis_liquidity.rs deleted file mode 100644 index e2a593cf..00000000 --- a/substrate/client/tests/genesis_liquidity.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::{time::Duration, collections::HashMap}; - -use serai_client::Serai; - -use serai_abi::primitives::{Amount, Coin, ExternalCoin, COINS, EXTERNAL_COINS, GENESIS_SRI}; - -use serai_client::genesis_liquidity::primitives::{ - GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES, -}; - -mod common; -use common::genesis_liquidity::set_up_genesis; - -serai_test_fast_epoch!( - genesis_liquidity: (|serai: Serai| async move { - test_genesis_liquidity(serai).await; - }) -); - -pub async fn test_genesis_liquidity(serai: Serai) { - // set up the genesis - let values = HashMap::from([ - (ExternalCoin::Monero, 184100), - (ExternalCoin::Ether, 4785000), - (ExternalCoin::Dai, 1500), - ]); - let (accounts, _) = set_up_genesis(&serai, &values).await; - - // wait until genesis is complete - while serai - .as_of_latest_finalized_block() - .await - .unwrap() - .genesis_liquidity() - .genesis_complete_block() - .await - .unwrap() - .is_none() - { - tokio::time::sleep(Duration::from_secs(1)).await; - } - - // check total SRI supply is +100M - // there are 6 endowed accounts in dev-net. Take this into consideration when checking - // for the total sri minted at this time. - let serai = serai.as_of_latest_finalized_block().await.unwrap(); - let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap(); - let endowed_amount: u64 = 1 << 60; - let total_sri = (6 * endowed_amount) + GENESIS_SRI; - assert_eq!(sri, Amount(total_sri)); - - // check genesis account has no coins, all transferred to pools. - for coin in COINS { - let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap(); - assert_eq!(amount.0, 0); - } - - // check pools has proper liquidity - let mut pool_amounts = HashMap::new(); - let mut total_value = 0u128; - for coin in EXTERNAL_COINS { - let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); - let value = if coin != ExternalCoin::Bitcoin { - (total_coin * u128::from(values[&coin])) / 10u128.pow(coin.decimals()) - } else { - total_coin - }; - - total_value += value; - pool_amounts.insert(coin, (total_coin, value)); - } - - // check distributed SRI per pool - let mut total_sri_distributed = 0u128; - for coin in EXTERNAL_COINS { - let sri = if coin == *EXTERNAL_COINS.last().unwrap() { - u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap() - } else { - (pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value - }; - total_sri_distributed += sri; - - let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap(); - assert_eq!(u128::from(reserves.0 .0), pool_amounts[&coin].0); // coin side - assert_eq!(u128::from(reserves.1 .0), sri); // SRI side - } - - // check each liquidity provider got liquidity tokens proportional to their value - for coin in EXTERNAL_COINS { - let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap(); - for (acc, amount) in &accounts[&coin] { - let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares; - - // since we can't test the ratios directly(due to integer division giving 0) - // we test whether they give the same result when multiplied by another constant. - // Following test ensures the account in fact has the right amount of shares. - let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares; - let amounts_ratio = - (INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap(); - - // we can tolerate 1 unit diff between them due to integer division. - if shares_ratio.abs_diff(amounts_ratio) == 1 { - shares_ratio = amounts_ratio; - } - - assert_eq!(shares_ratio, amounts_ratio); - } - } - - // TODO: test remove the liq before/after genesis ended. -} diff --git a/substrate/client/tests/serai-rpc.rs b/substrate/client/tests/serai-rpc.rs deleted file mode 100644 index 8ab59426..00000000 --- a/substrate/client/tests/serai-rpc.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::str::FromStr; - -use scale::Decode; -use zeroize::Zeroizing; - -use ciphersuite::{ - group::{ff::Field, GroupEncoding}, - WrappedGroup, -}; -use dalek_ff_group::Ed25519; -use ciphersuite_kp256::Secp256k1; - -use sp_core::{ - Pair as PairTrait, - sr25519::{Public, Pair}, -}; - -use serai_abi::{ - in_instructions::primitives::Shorthand, - primitives::{ - insecure_pair_from_name, ExternalBalance, ExternalCoin, ExternalNetworkId, QuotePriceParams, - Amount, - }, - validator_sets::primitives::{ExternalValidatorSet, KeyPair, Session}, -}; -use serai_client::{Serai, SeraiAddress}; - -use rand_core::{RngCore, OsRng}; - -mod common; -use common::{validator_sets::set_keys, in_instructions::mint_coin, dex::add_liquidity}; - -serai_test!( - external_address: (|serai: Serai| async move { - test_external_address(serai).await; - }) - - encoded_shorthand: (|serai: Serai| async move { - test_encoded_shorthand(serai).await; - }) - - dex_quote_price: (|serai: Serai| async move { - test_dex_quote_price(serai).await; - }) -); - -async fn set_network_keys( - serai: &Serai, - set: ExternalValidatorSet, - pairs: &[Pair], -) { - // Ristretto key - let mut ristretto_key = [0; 32]; - OsRng.fill_bytes(&mut ristretto_key); - - // network key - let network_priv_key = Zeroizing::new(C::F::random(&mut OsRng)); - let network_key = (C::generator() * *network_priv_key).to_bytes().as_ref().to_vec(); - - let key_pair = KeyPair(Public(ristretto_key), network_key.try_into().unwrap()); - let _ = set_keys(serai, set, key_pair, pairs).await; -} - -async fn test_external_address(serai: Serai) { - let pair = insecure_pair_from_name("Alice"); - - // set btc keys - let network = ExternalNetworkId::Bitcoin; - set_network_keys::( - &serai, - ExternalValidatorSet { session: Session(0), network }, - core::slice::from_ref(&pair), - ) - .await; - - // get the address from the node - let btc_address: String = serai.external_network_address(network).await.unwrap(); - - // make sure it is a valid address - let _ = bitcoin::Address::from_str(&btc_address) - .unwrap() - .require_network(bitcoin::Network::Bitcoin) - .unwrap(); - - // set monero keys - let network = ExternalNetworkId::Monero; - set_network_keys::( - &serai, - ExternalValidatorSet { session: Session(0), network }, - &[pair], - ) - .await; - - // get the address from the node - let xmr_address: String = serai.external_network_address(network).await.unwrap(); - - // make sure it is a valid address - let _ = monero_address::MoneroAddress::from_str(monero_address::Network::Mainnet, &xmr_address) - .unwrap(); -} - -async fn test_encoded_shorthand(serai: Serai) { - let shorthand = Shorthand::transfer(None, SeraiAddress::new([0u8; 32])); - let encoded = serai.encoded_shorthand(shorthand.clone()).await.unwrap(); - - assert_eq!(Shorthand::decode::<&[u8]>(&mut encoded.as_slice()).unwrap(), shorthand); -} - -async fn test_dex_quote_price(serai: Serai) { - // make a liquid pool to get the quote on - let coin1 = ExternalCoin::Bitcoin; - let coin2 = ExternalCoin::Monero; - let amount1 = Amount(10u64.pow(coin1.decimals())); - let amount2 = Amount(10u64.pow(coin2.decimals())); - let pair = insecure_pair_from_name("Ferdie"); - - // mint sriBTC in the account so that we can add liq. - // Ferdie account is already pre-funded with SRI. - mint_coin( - &serai, - ExternalBalance { coin: coin1, amount: amount1 }, - 0, - pair.clone().public().into(), - ) - .await; - - // add liquidity - let coin_amount = Amount(amount1.0 / 2); - let sri_amount = Amount(amount1.0 / 2); - let _ = add_liquidity(&serai, coin1, coin_amount, sri_amount, 0, pair.clone()).await; - - // same for xmr - mint_coin( - &serai, - ExternalBalance { coin: coin2, amount: amount2 }, - 0, - pair.clone().public().into(), - ) - .await; - - // add liquidity - let coin_amount = Amount(amount2.0 / 2); - let sri_amount = Amount(amount2.0 / 2); - let _ = add_liquidity(&serai, coin2, coin_amount, sri_amount, 1, pair.clone()).await; - - // price for BTC -> SRI -> XMR path - let params = QuotePriceParams { - coin1: coin1.into(), - coin2: coin2.into(), - amount: coin_amount.0 / 2, - include_fee: true, - exact_in: true, - }; - - let res = serai.quote_price(params).await.unwrap(); - assert!(res > 0); -} diff --git a/substrate/client/tests/time.rs b/substrate/client/tests/time.rs deleted file mode 100644 index 0a8764dd..00000000 --- a/substrate/client/tests/time.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::time::{Duration, SystemTime}; - -use tokio::time::sleep; - -use serai_client::Serai; - -mod common; - -serai_test!( - time: (|serai: Serai| async move { - let mut number = serai.latest_finalized_block().await.unwrap().number(); - let mut done = 0; - while done < 3 { - // Wait for the next block - let block = serai.latest_finalized_block().await.unwrap(); - if block.number() == number { - sleep(Duration::from_secs(1)).await; - continue; - } - number = block.number(); - - // Make sure the time we extract from the block is within 5 seconds of now - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); - assert!(now.saturating_sub(block.time().unwrap()) < 5); - done += 1; - } - }) -); diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs deleted file mode 100644 index 32f5d481..00000000 --- a/substrate/client/tests/validator_sets.rs +++ /dev/null @@ -1,428 +0,0 @@ -use rand_core::{RngCore, OsRng}; - -use sp_core::{ - sr25519::{Public, Pair}, - Pair as PairTrait, -}; - -use serai_client::{ - primitives::{ - FAST_EPOCH_DURATION, TARGET_BLOCK_TIME, NETWORKS, BlockHash, ExternalNetworkId, NetworkId, - EmbeddedEllipticCurve, Amount, insecure_pair_from_name, - }, - validator_sets::{ - primitives::{Session, ExternalValidatorSet, ValidatorSet, KeyPair}, - ValidatorSetsEvent, - }, - in_instructions::{ - primitives::{Batch, SignedBatch, batch_message}, - SeraiInInstructions, - }, - Serai, -}; - -mod common; -use common::{ - tx::publish_tx, - validator_sets::{set_embedded_elliptic_curve_key, allocate_stake, deallocate_stake, set_keys}, -}; - -fn get_random_key_pair() -> KeyPair { - let mut ristretto_key = [0; 32]; - OsRng.fill_bytes(&mut ristretto_key); - let mut external_key = vec![0; 33]; - OsRng.fill_bytes(&mut external_key); - KeyPair(Public(ristretto_key), external_key.try_into().unwrap()) -} - -async fn get_ordered_keys(serai: &Serai, network: NetworkId, accounts: &[Pair]) -> Vec { - // retrieve the current session validators so that we know the order of the keys - // that is necessary for the correct musig signature. - let validators = serai - .as_of_latest_finalized_block() - .await - .unwrap() - .validator_sets() - .active_network_validators(network) - .await - .unwrap(); - - // collect the pairs of the validators - let mut pairs = vec![]; - for v in validators { - let p = accounts.iter().find(|pair| pair.public() == v).unwrap().clone(); - pairs.push(p); - } - - pairs -} - -serai_test!( - set_keys_test: (|serai: Serai| async move { - let network = ExternalNetworkId::Bitcoin; - let set = ExternalValidatorSet { session: Session(0), network }; - - let pair = insecure_pair_from_name("Alice"); - let public = pair.public(); - - // Neither of these keys are validated - // The external key is infeasible to validate on-chain, the Ristretto key is feasible - // TODO: Should the Ristretto key be validated? - let key_pair = get_random_key_pair(); - - // Make sure the genesis is as expected - assert_eq!( - serai - .as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash()) - .validator_sets() - .new_set_events() - .await - .unwrap(), - NETWORKS - .iter() - .copied() - .map(|network| ValidatorSetsEvent::NewSet { - set: ValidatorSet { session: Session(0), network } - }) - .collect::>(), - ); - - { - let vs_serai = serai.as_of_latest_finalized_block().await.unwrap(); - let vs_serai = vs_serai.validator_sets(); - let participants = vs_serai.participants(set.network.into()).await - .unwrap() - .unwrap() - .into_iter() - .map(|(k, _)| k) - .collect::>(); - let participants_ref: &[_] = participants.as_ref(); - assert_eq!(participants_ref, [public].as_ref()); - } - - let block = set_keys(&serai, set, key_pair.clone(), &[pair]).await; - - // While the set_keys function should handle this, it's beneficial to - // independently test it - let serai = serai.as_of(block); - let serai = serai.validator_sets(); - assert_eq!( - serai.key_gen_events().await.unwrap(), - vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] - ); - assert_eq!(serai.keys(set).await.unwrap(), Some(key_pair)); - }) -); - -#[tokio::test] -async fn validator_set_rotation() { - use dockertest::{ - PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, - TestBodySpecification, DockerTest, - }; - use std::collections::HashMap; - - serai_docker_tests::build("serai-fast-epoch".to_string()); - - let handle = |name| format!("serai_client-serai_node-{name}"); - let composition = |name| { - TestBodySpecification::with_image( - Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never), - ) - .replace_cmd(vec![ - "serai-node".to_string(), - "--unsafe-rpc-external".to_string(), - "--rpc-cors".to_string(), - "all".to_string(), - "--chain".to_string(), - "local".to_string(), - format!("--{name}"), - ]) - .replace_env(HashMap::from([ - ("RUST_LOG".to_string(), "runtime=debug".to_string()), - ("KEY".to_string(), " ".to_string()), - ])) - .set_publish_all_ports(true) - .set_handle(handle(name)) - .set_start_policy(StartPolicy::Strict) - .set_log_options(Some(LogOptions { - action: LogAction::Forward, - policy: LogPolicy::Always, - source: LogSource::Both, - })) - }; - - let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); - test.provide_container(composition("alice")); - test.provide_container(composition("bob")); - test.provide_container(composition("charlie")); - test.provide_container(composition("dave")); - test.provide_container(composition("eve")); - test - .run_async(|ops| async move { - // Sleep until the Substrate RPC starts - let alice = handle("alice"); - let alice_rpc = ops.handle(&alice).host_port(9944).unwrap(); - let alice_rpc = format!("http://{}:{}", alice_rpc.0, alice_rpc.1); - - // Sleep for some time - tokio::time::sleep(core::time::Duration::from_secs(20)).await; - let serai = Serai::new(alice_rpc.clone()).await.unwrap(); - - // Make sure the genesis is as expected - assert_eq!( - serai - .as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash()) - .validator_sets() - .new_set_events() - .await - .unwrap(), - NETWORKS - .iter() - .copied() - .map(|network| ValidatorSetsEvent::NewSet { - set: ValidatorSet { session: Session(0), network } - }) - .collect::>(), - ); - - // genesis accounts - let accounts = vec![ - insecure_pair_from_name("Alice"), - insecure_pair_from_name("Bob"), - insecure_pair_from_name("Charlie"), - insecure_pair_from_name("Dave"), - insecure_pair_from_name("Eve"), - ]; - - // amounts for single key share per network - let key_shares = HashMap::from([ - (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))), - (NetworkId::External(ExternalNetworkId::Bitcoin), Amount(1_000_000 * 10_u64.pow(8))), - (NetworkId::External(ExternalNetworkId::Monero), Amount(100_000 * 10_u64.pow(8))), - (NetworkId::External(ExternalNetworkId::Ethereum), Amount(1_000_000 * 10_u64.pow(8))), - ]); - - // genesis participants per network - #[allow(clippy::redundant_closure_for_method_calls)] - let default_participants = - accounts[.. 4].to_vec().iter().map(|pair| pair.public()).collect::>(); - let mut participants = HashMap::from([ - (NetworkId::Serai, default_participants.clone()), - (NetworkId::External(ExternalNetworkId::Bitcoin), default_participants.clone()), - (NetworkId::External(ExternalNetworkId::Monero), default_participants.clone()), - (NetworkId::External(ExternalNetworkId::Ethereum), default_participants), - ]); - - // test the set rotation - for (i, network) in NETWORKS.into_iter().enumerate() { - let participants = participants.get_mut(&network).unwrap(); - - // we start the chain with 4 default participants that has a single key share each - participants.sort(); - verify_session_and_active_validators(&serai, network, 0, participants).await; - - // add 1 participant - let last_participant = accounts[4].clone(); - - // If this is the first iteration, set embedded elliptic curve keys - if i == 0 { - for (i, embedded_elliptic_curve) in - [EmbeddedEllipticCurve::Embedwards25519, EmbeddedEllipticCurve::Secq256k1] - .into_iter() - .enumerate() - { - set_embedded_elliptic_curve_key( - &serai, - &last_participant, - embedded_elliptic_curve, - vec![ - 0; - match embedded_elliptic_curve { - EmbeddedEllipticCurve::Embedwards25519 => 32, - EmbeddedEllipticCurve::Secq256k1 => 33, - } - ] - .try_into() - .unwrap(), - i.try_into().unwrap(), - ) - .await; - } - } - - let hash = allocate_stake( - &serai, - network, - key_shares[&network], - &last_participant, - (2 + i).try_into().unwrap(), - ) - .await; - participants.push(last_participant.public()); - // the session at which set changes becomes active - let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await; - - // set the keys if it is an external set - if network != NetworkId::Serai { - let set = - ExternalValidatorSet { session: Session(0), network: network.try_into().unwrap() }; - let key_pair = get_random_key_pair(); - let pairs = get_ordered_keys(&serai, network, &accounts).await; - set_keys(&serai, set, key_pair, &pairs).await; - } - - // verify - participants.sort(); - verify_session_and_active_validators(&serai, network, activation_session, participants) - .await; - - // remove 1 participant - let participant_to_remove = accounts[1].clone(); - let hash = deallocate_stake( - &serai, - network, - key_shares[&network], - &participant_to_remove, - i.try_into().unwrap(), - ) - .await; - participants.swap_remove( - participants.iter().position(|k| *k == participant_to_remove.public()).unwrap(), - ); - let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await; - - if network != NetworkId::Serai { - // set the keys if it is an external set - let set = - ExternalValidatorSet { session: Session(1), network: network.try_into().unwrap() }; - - // we need the whole substrate key pair to sign the batch - let (substrate_pair, key_pair) = { - let pair = insecure_pair_from_name("session-1-key-pair"); - let public = pair.public(); - - let mut external_key = vec![0; 33]; - OsRng.fill_bytes(&mut external_key); - - (pair, KeyPair(public, external_key.try_into().unwrap())) - }; - let pairs = get_ordered_keys(&serai, network, &accounts).await; - set_keys(&serai, set, key_pair, &pairs).await; - - // provide a batch to complete the handover and retire the previous set - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - let batch = Batch { - network: network.try_into().unwrap(), - id: 0, - external_network_block_hash: block_hash, - instructions: vec![], - }; - publish_tx( - &serai, - &SeraiInInstructions::execute_batch(SignedBatch { - batch: batch.clone(), - signature: substrate_pair.sign(&batch_message(&batch)), - }), - ) - .await; - } - - // verify - participants.sort(); - verify_session_and_active_validators(&serai, network, activation_session, participants) - .await; - - // check pending deallocations - let pending = serai - .as_of_latest_finalized_block() - .await - .unwrap() - .validator_sets() - .pending_deallocations( - network, - participant_to_remove.public(), - Session(activation_session + 1), - ) - .await - .unwrap(); - assert_eq!(pending, Some(key_shares[&network])); - } - }) - .await; -} - -async fn session_for_block(serai: &Serai, block: [u8; 32], network: NetworkId) -> u32 { - serai.as_of(block).validator_sets().session(network).await.unwrap().unwrap().0 -} - -async fn verify_session_and_active_validators( - serai: &Serai, - network: NetworkId, - session: u32, - participants: &[Public], -) { - // wait until the active session. - let block = tokio::time::timeout( - core::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2), - async move { - loop { - let mut block = serai.latest_finalized_block_hash().await.unwrap(); - if session_for_block(serai, block, network).await < session { - // Sleep a block - tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await; - continue; - } - while session_for_block(serai, block, network).await > session { - block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0; - } - assert_eq!(session_for_block(serai, block, network).await, session); - break block; - } - }, - ) - .await - .unwrap(); - let serai_for_block = serai.as_of(block); - - // verify session - let s = serai_for_block.validator_sets().session(network).await.unwrap().unwrap(); - assert_eq!(s.0, session); - - // verify participants - let mut validators = - serai_for_block.validator_sets().active_network_validators(network).await.unwrap(); - validators.sort(); - assert_eq!(validators, participants); - - // make sure finalization continues as usual after the changes - let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number; - tokio::time::timeout(core::time::Duration::from_secs(TARGET_BLOCK_TIME * 10), async move { - let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number; - while finalized_block <= current_finalized_block + 2 { - tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await; - finalized_block = serai.latest_finalized_block().await.unwrap().header.number; - } - }) - .await - .unwrap(); - - // TODO: verify key shares as well? -} - -async fn get_session_at_which_changes_activate( - serai: &Serai, - network: NetworkId, - hash: [u8; 32], -) -> u32 { - let session = session_for_block(serai, hash, network).await; - - // changes should be active in the next session - if network == NetworkId::Serai { - // it takes 1 extra session for serai net to make the changes active. - session + 2 - } else { - session + 1 - } -}