diff --git a/Cargo.lock b/Cargo.lock index 3a0b32999..3a0297c3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10253,12 +10253,13 @@ dependencies = [ [[package]] name = "substrate-stellar-sdk" version = "0.2.4" -source = "git+https://github.com/pendulum-chain/substrate-stellar-sdk?branch=polkadot-v0.9.40#dc4212fc0b9c29d9fd370cde7ba666172de8fb7b" +source = "git+https://github.com/pendulum-chain/substrate-stellar-sdk?branch=polkadot-v0.9.40#0030e2aaedef7b7cb89108a64a7b9569a20d8044" dependencies = [ "base64 0.13.1", "hex", "lazy_static", "num-rational", + "scale-info", "serde", "serde_json", "sha2 0.9.9", diff --git a/clients/runtime/Cargo.toml b/clients/runtime/Cargo.toml index e87011441..d43a2f4ff 100644 --- a/clients/runtime/Cargo.toml +++ b/clients/runtime/Cargo.toml @@ -14,9 +14,9 @@ testing-utils = [ "tempdir", "rand", "testchain", - "testchain-runtime", + "testchain-runtime/testing-utils", "subxt-client", - "oracle" + "oracle/testing-utils" ] [dependencies] diff --git a/clients/runtime/src/rpc.rs b/clients/runtime/src/rpc.rs index a2b34c229..54018c2c3 100644 --- a/clients/runtime/src/rpc.rs +++ b/clients/runtime/src/rpc.rs @@ -1437,17 +1437,24 @@ impl ReplacePallet for SpacewalkParachain { #[async_trait] pub trait StellarRelayPallet { - async fn is_public_network(&self) -> Result; + async fn is_public_network(&self) -> bool; } #[async_trait] impl StellarRelayPallet for SpacewalkParachain { - async fn is_public_network(&self) -> Result { + async fn is_public_network(&self) -> bool { let address = metadata::constants().stellar_relay().is_public_network(); let result = self.api.constants().at(&address); match result { - Ok(result) => Ok(result), - Err(_) => Err(Error::ConstantNotFound("is_public_network".to_string())), + Ok(result) => result, + Err(e) => { + // Sometimes the fetch fails with 'StorageItemNotFound' error. + // We assume public network by default + log::warn!( + "Failed to fetch public network status from parachain: {e:?}. Assuming public network." + ); + true + }, } } } diff --git a/clients/runtime/src/types.rs b/clients/runtime/src/types.rs index 7c365b376..751575767 100644 --- a/clients/runtime/src/types.rs +++ b/clients/runtime/src/types.rs @@ -330,9 +330,9 @@ mod dispatch_error { DispatchError::Arithmetic(arithmetic_error.into()), RichDispatchError::Transactional(transactional_error) => DispatchError::Transactional(transactional_error.into()), - RichDispatchError::Exhausted | - sp_runtime::DispatchError::Corruption | - sp_runtime::DispatchError::Unavailable => todo!(), + RichDispatchError::Exhausted => DispatchError::Exhausted, + sp_runtime::DispatchError::Corruption => DispatchError::Corruption, + sp_runtime::DispatchError::Unavailable => DispatchError::Unavailable, } } } diff --git a/clients/stellar-relay-lib/src/connection/error.rs b/clients/stellar-relay-lib/src/connection/error.rs index 334600383..261963356 100644 --- a/clients/stellar-relay-lib/src/connection/error.rs +++ b/clients/stellar-relay-lib/src/connection/error.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] //todo: remove after being tested and implemented - use crate::{connection::xdr_converter::Error as XDRError, helper::error_to_string}; use substrate_stellar_sdk::{types::ErrorCode, StellarSdkError}; use tokio::sync; diff --git a/clients/stellar-relay-lib/src/connection/xdr_converter.rs b/clients/stellar-relay-lib/src/connection/xdr_converter.rs index cb25e724b..9ff390188 100644 --- a/clients/stellar-relay-lib/src/connection/xdr_converter.rs +++ b/clients/stellar-relay-lib/src/connection/xdr_converter.rs @@ -1,10 +1,8 @@ -#![allow(dead_code)] //todo: remove after being tested and implemented - use crate::sdk::types::{ AuthenticatedMessage, AuthenticatedMessageV0, HmacSha256Mac, MessageType, StellarMessage, }; use std::fmt::Debug; -use substrate_stellar_sdk::XdrCodec; +use substrate_stellar_sdk::{parse_stellar_type, StellarSdkError, XdrCodec}; #[derive(Debug, Eq, PartialEq, err_derive::Error)] pub enum Error { @@ -14,10 +12,19 @@ pub enum Error { #[error(display = "Message Version: Unsupported")] UnsupportedMessageVersion, + #[error(display = "Sdk Error: {}", _0)] + SdkError(String), + #[error(display = "Decode Error: {}", _0)] DecodeError(String), } +impl From for Error { + fn from(value: StellarSdkError) -> Self { + Error::SdkError(format!("{value:#?}")) + } +} + /// The 1st 4 bytes determines the byte length of the next stellar message. /// Returns 0 if the array of u8 is less than 4 bytes, or exceeds the max of u32. pub(crate) fn get_xdr_message_length(data: &[u8]) -> usize { @@ -37,36 +44,6 @@ pub(crate) fn from_authenticated_message(message: &AuthenticatedMessage) -> Resu message_to_bytes(message) } -// todo: move to substrate-stellar-sdk -/// To easily convert any bytes to a Stellar type. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// use substrate_stellar_sdk::types::Auth; -/// use stellar_relay_lib::parse_stellar_type; -/// let auth_xdr = [0, 0, 0, 1]; -/// let result = parse_stellar_type!(auth_xdr,Auth); -/// assert_eq!(result, Ok(Auth { flags: 1 })) -/// ``` -#[macro_export] -macro_rules! parse_stellar_type { - ($ref:ident, $struct_str:ident) => {{ - use $crate::{ - sdk::{types::$struct_str, XdrCodec}, - xdr_converter::Error, - }; - - let ret: Result<$struct_str, Error> = $struct_str::from_xdr($ref).map_err(|e| { - log::error!("decode error: {:?}", e); - Error::DecodeError(stringify!($struct_str).to_string()) - }); - ret - }}; -} - /// Parses the xdr message into `AuthenticatedMessageV0`. /// When successful, returns a tuple of the message and the `MessageType`. pub(crate) fn parse_authenticated_message( @@ -92,7 +69,7 @@ pub(crate) fn parse_authenticated_message( } fn parse_stellar_message(xdr_message: &[u8]) -> Result { - parse_stellar_type!(xdr_message, StellarMessage) + parse_stellar_type!(xdr_message, StellarMessage).map_err(|e| e.into()) } fn parse_message_version(xdr_message: &[u8]) -> Result { @@ -104,11 +81,11 @@ fn parse_sequence(xdr_message: &[u8]) -> Result { } fn parse_hmac(xdr_message: &[u8]) -> Result { - parse_stellar_type!(xdr_message, HmacSha256Mac) + parse_stellar_type!(xdr_message, HmacSha256Mac).map_err(|e| e.into()) } fn parse_message_type(xdr_message: &[u8]) -> Result { - parse_stellar_type!(xdr_message, MessageType) + parse_stellar_type!(xdr_message, MessageType).map_err(|e| e.into()) } /// Returns XDR format of the message or @@ -135,21 +112,19 @@ pub fn log_decode_error(source: &str, error: T) -> Error { Error::DecodeError(source.to_string()) } -// extra function. -fn is_xdr_complete_message(data: &[u8], message_len: usize) -> bool { - data.len() - 4 >= message_len -} - -// extra function -fn get_message(data: &[u8], message_len: usize) -> (Vec, Vec) { - (data[4..(message_len + 4)].to_owned(), data[0..(message_len + 4)].to_owned()) -} - #[cfg(test)] mod test { - use crate::connection::xdr_converter::{ - get_message, get_xdr_message_length, is_xdr_complete_message, parse_authenticated_message, - }; + use crate::connection::xdr_converter::{get_xdr_message_length, parse_authenticated_message}; + + // extra function. + fn is_xdr_complete_message(data: &[u8], message_len: usize) -> bool { + data.len() - 4 >= message_len + } + + // extra function + fn get_message(data: &[u8], message_len: usize) -> (Vec, Vec) { + (data[4..(message_len + 4)].to_owned(), data[0..(message_len + 4)].to_owned()) + } #[test] fn get_xdr_message_length_success() { @@ -165,8 +140,7 @@ mod test { base64::STANDARD ).expect("should be able to decode to bytes"); - //todo: once the authenticatedmessagev0 type is solved, continue the test - let _ = parse_authenticated_message(&msg); + assert!(parse_authenticated_message(&msg).is_ok()); } #[test] diff --git a/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet b/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet new file mode 100644 index 000000000..169351c64 --- /dev/null +++ b/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet @@ -0,0 +1 @@ +SDNQJEIRSA6YF5JNS6LQLCBF2XVWZ2NJV3YLC322RGIBJIJRIRGWKLEF \ No newline at end of file diff --git a/clients/vault/resources/secretkey/stellar_secretkey_mainnet b/clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet_with_currency similarity index 100% rename from clients/vault/resources/secretkey/stellar_secretkey_mainnet rename to clients/vault/resources/secretkey/mainnet/stellar_secretkey_mainnet_with_currency diff --git a/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet b/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet new file mode 100644 index 000000000..169351c64 --- /dev/null +++ b/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet @@ -0,0 +1 @@ +SDNQJEIRSA6YF5JNS6LQLCBF2XVWZ2NJV3YLC322RGIBJIJRIRGWKLEF \ No newline at end of file diff --git a/clients/vault/resources/secretkey/stellar_secretkey_testnet b/clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet_with_currency similarity index 100% rename from clients/vault/resources/secretkey/stellar_secretkey_testnet rename to clients/vault/resources/secretkey/testnet/stellar_secretkey_testnet_with_currency diff --git a/clients/vault/resources/test/tx_sets/92886_92900 b/clients/vault/resources/test/tx_sets/92886_92900 new file mode 100644 index 000000000..ca331a3ac Binary files /dev/null and b/clients/vault/resources/test/tx_sets/92886_92900 differ diff --git a/clients/vault/resources/test/tx_sets/92901_92915 b/clients/vault/resources/test/tx_sets/92901_92915 new file mode 100644 index 000000000..bdc35b200 Binary files /dev/null and b/clients/vault/resources/test/tx_sets/92901_92915 differ diff --git a/clients/vault/resources/test/tx_sets/92916_92930 b/clients/vault/resources/test/tx_sets/92916_92930 new file mode 100644 index 000000000..a1a7eb219 Binary files /dev/null and b/clients/vault/resources/test/tx_sets/92916_92930 differ diff --git a/clients/vault/resources/test/tx_sets_for_testing/92931_92945 b/clients/vault/resources/test/tx_sets_for_testing/92931_92945 new file mode 100644 index 000000000..5ad4ea883 Binary files /dev/null and b/clients/vault/resources/test/tx_sets_for_testing/92931_92945 differ diff --git a/clients/vault/src/issue.rs b/clients/vault/src/issue.rs index e1dc543d9..bf1ef6f3e 100644 --- a/clients/vault/src/issue.rs +++ b/clients/vault/src/issue.rs @@ -14,10 +14,7 @@ use wallet::{ types::FilterWith, LedgerTxEnvMap, Slot, SlotTask, SlotTaskStatus, TransactionResponse, }; -use crate::{ - oracle::{types::Slot as OracleSlot, OracleAgent}, - ArcRwLock, Error, Event, -}; +use crate::{oracle::OracleAgent, ArcRwLock, Error, Event}; fn is_vault(p1: &PublicKey, p2_raw: [u8; 32]) -> bool { return *p1.as_binary() == p2_raw @@ -307,7 +304,6 @@ pub async fn execute_issue( slot: Slot, sender: tokio::sync::oneshot::Sender, ) { - let slot = OracleSlot::from(slot); // Get the proof of the given slot let proof = match oracle_agent.get_proof(slot).await { diff --git a/clients/vault/src/oracle/agent.rs b/clients/vault/src/oracle/agent.rs index 55dfabac6..1b8f18fe8 100644 --- a/clients/vault/src/oracle/agent.rs +++ b/clients/vault/src/oracle/agent.rs @@ -13,11 +13,9 @@ use stellar_relay_lib::{ }; use crate::oracle::{ - collector::ScpMessageCollector, - errors::Error, - types::{Slot, StellarMessageSender}, - AddTxSet, Proof, + collector::ScpMessageCollector, errors::Error, types::StellarMessageSender, AddTxSet, Proof, }; +use wallet::Slot; pub struct OracleAgent { collector: Arc>, @@ -207,7 +205,7 @@ impl OracleAgent { #[cfg(test)] mod tests { use crate::oracle::{ - get_random_secret_key, get_test_secret_key, specific_stellar_relay_config, + get_random_secret_key, get_secret_key, specific_stellar_relay_config, traits::ArchiveStorage, ScpArchiveStorage, TransactionsArchiveStorage, }; @@ -259,7 +257,7 @@ mod tests { let shutdown_sender = ShutdownSender::new(); let agent = start_oracle_agent( specific_stellar_relay_config(true, 1), - &get_test_secret_key(true), + &get_secret_key(true, true), shutdown_sender, ) .await @@ -299,7 +297,7 @@ mod tests { let shutdown_sender = ShutdownSender::new(); let agent = - start_oracle_agent(modified_config, &get_test_secret_key(true), shutdown_sender) + start_oracle_agent(modified_config, &get_secret_key(true, true), shutdown_sender) .await .expect("Failed to start agent"); @@ -326,7 +324,7 @@ mod tests { StellarOverlayConfig { stellar_history_archive_urls: vec![], ..base_config }; let shutdown = ShutdownSender::new(); - let agent = start_oracle_agent(modified_config, &get_test_secret_key(true), shutdown) + let agent = start_oracle_agent(modified_config, &get_secret_key(true, true), shutdown) .await .expect("Failed to start agent"); diff --git a/clients/vault/src/oracle/collector/collector.rs b/clients/vault/src/oracle/collector/collector.rs index 97f014c98..c17879d4f 100644 --- a/clients/vault/src/oracle/collector/collector.rs +++ b/clients/vault/src/oracle/collector/collector.rs @@ -2,16 +2,15 @@ use std::{default::Default, sync::Arc}; use parking_lot::{lock_api::RwLockReadGuard, RawRwLock, RwLock}; -use runtime::stellar::types::GeneralizedTransactionSet; - use stellar_relay_lib::sdk::{ network::{Network, PUBLIC_NETWORK, TEST_NETWORK}, - types::{ScpEnvelope, TransactionSet}, + types::{GeneralizedTransactionSet, ScpEnvelope, TransactionSet}, TransactionSetType, }; +use wallet::Slot; use crate::oracle::types::{ - EnvelopesMap, LimitedFifoMap, Slot, TxSetHash, TxSetHashAndSlotMap, TxSetMap, + EnvelopesMap, LimitedFifoMap, TxSetHash, TxSetHashAndSlotMap, TxSetMap, }; /// Collects all ScpMessages and the TxSets. @@ -31,7 +30,7 @@ pub struct ScpMessageCollector { txset_and_slot_map: Arc>, /// The last slot with an SCPEnvelope - last_slot_index: u64, + last_slot_index: Slot, public_network: bool, @@ -236,7 +235,7 @@ mod test { collector::{collector::AddTxSet, ScpMessageCollector}, random_stellar_relay_config, traits::FileHandler, - EnvelopesFileHandler, + EnvelopesFileHandler, TxSetsFileHandler, }; fn open_file(file_name: &str) -> Vec { @@ -365,7 +364,6 @@ mod test { assert_eq!(res, 15); } - // todo: update this with a new txset sample #[test] fn remove_data_works() { let collector = ScpMessageCollector::new(false, stellar_history_archive_urls()); @@ -374,47 +372,42 @@ mod test { let env_map = EnvelopesFileHandler::get_map_from_archives(env_slot).expect("should return a map"); - // let txset_slot = 42867088; - // let txsets_map = - // TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); + let txset_slot = 92910; + let txsets_map = + TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); - // collector.watch_slot(env_slot); collector.envelopes_map.write().append(env_map); - // let txset = txsets_map.get(&txset_slot).expect("should return a tx set"); - // collector.txset_map.write().insert(env_slot, txset.clone()); + let txset = txsets_map.get(&txset_slot).expect("should return a tx set"); + collector.txset_map.write().insert(env_slot, txset.clone()); assert!(collector.envelopes_map.read().contains(&env_slot)); - //assert!(collector.txset_map.read().contains(&env_slot)); - // assert!(collector.slot_watchlist.read().contains_key(&env_slot)); + assert!(collector.txset_map.read().contains(&env_slot)); collector.remove_data(&env_slot); + collector.remove_data(&txset_slot); assert!(!collector.envelopes_map.read().contains(&env_slot)); - //assert!(!collector.txset_map.read().contains(&env_slot)); - } - - // todo: update this with a new txset sample - // #[test] - // fn is_txset_new_works() { - // let collector = ScpMessageCollector::new(false, stellar_history_archive_urls()); - // - // let txset_slot = 42867088; - // let txsets_map = - // TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); - // - // let txsets_size = txsets_map.len(); - // println!("txsets size: {}", txsets_size); - // - // collector.txset_map.write().append(txsets_map); - // - // collector.txset_and_slot_map.write().insert([0; 32], 0); - // - // // these didn't exist yet. - // assert!(collector.is_txset_new(&[1; 32], &5)); - // - // // the hash exists - // assert!(!collector.is_txset_new(&[0; 32], &6)); - // // the slot exists - // assert!(!collector.is_txset_new(&[3; 32], &txset_slot)); - // } + assert!(!collector.txset_map.read().contains(&txset_slot)); + } + + #[test] + fn is_txset_new_works() { + let collector = ScpMessageCollector::new(false, stellar_history_archive_urls()); + + let txset_slot = 92900; + let txsets_map = + TxSetsFileHandler::get_map_from_archives(txset_slot).expect("should return a map"); + + collector.txset_map.write().append(txsets_map); + + collector.txset_and_slot_map.write().insert([0; 32], 0); + + // these didn't exist yet. + assert!(collector.is_txset_new(&[1; 32], &5)); + + // the hash exists + assert!(!collector.is_txset_new(&[0; 32], &6)); + // the slot exists + assert!(!collector.is_txset_new(&[3; 32], &txset_slot)); + } } diff --git a/clients/vault/src/oracle/collector/proof_builder.rs b/clients/vault/src/oracle/collector/proof_builder.rs index 2bbc00ac0..24ca786fb 100644 --- a/clients/vault/src/oracle/collector/proof_builder.rs +++ b/clients/vault/src/oracle/collector/proof_builder.rs @@ -7,12 +7,13 @@ use stellar_relay_lib::sdk::{ types::{ScpEnvelope, ScpHistoryEntry, ScpStatementPledges, StellarMessage}, InitExt, TransactionSetType, XdrCodec, }; +use wallet::Slot; use crate::oracle::{ constants::{get_min_externalized_messages, MAX_SLOTS_TO_REMEMBER}, traits::ArchiveStorage, types::StellarMessageSender, - ScpArchiveStorage, ScpMessageCollector, Slot, TransactionsArchiveStorage, + ScpArchiveStorage, ScpMessageCollector, TransactionsArchiveStorage, }; /// Returns true if the SCP messages for a given slot are still recoverable from the overlay diff --git a/clients/vault/src/oracle/storage/impls.rs b/clients/vault/src/oracle/storage/impls.rs index e4dd33677..b86cc6f94 100644 --- a/clients/vault/src/oracle/storage/impls.rs +++ b/clients/vault/src/oracle/storage/impls.rs @@ -7,9 +7,10 @@ use stellar_relay_lib::sdk::{ }; use crate::oracle::{ - storage::traits::*, EnvelopesFileHandler, EnvelopesMap, Error, Filename, SerializedData, Slot, + storage::traits::*, EnvelopesFileHandler, EnvelopesMap, Error, Filename, SerializedData, SlotEncodedMap, TxSetMap, TxSetsFileHandler, }; +use wallet::Slot; use super::{ScpArchiveStorage, TransactionsArchiveStorage}; @@ -166,11 +167,11 @@ mod test { traits::{FileHandler, FileHandlerExt}, EnvelopesFileHandler, }, - types::Slot, - TransactionsArchiveStorage, + TransactionsArchiveStorage, TxSetsFileHandler, }; use super::ScpArchiveStorage; + use wallet::Slot; impl Default for ScpArchiveStorage { fn default() -> Self { @@ -227,37 +228,36 @@ mod test { assert_eq!(&file_name, &expected_name); } - // todo: enable after generating a new sample of txsetmap // ---------------- TESTS FOR TX SETS ----------- // finding first slot - // { - // let slot = 42867088; - // let expected_name = format!("{}_42867102", slot); - // let file_name = - // TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); - // assert_eq!(&file_name, &expected_name); - // } - // - // // finding slot in the middle of the file - // { - // let first_slot = 42867103; - // let expected_name = format!("{}_42867118", first_slot); - // let slot = first_slot + 10; - // - // let file_name = - // TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); - // assert_eq!(&file_name, &expected_name); - // } - // - // // finding slot at the end of the file - // { - // let slot = 42867134; - // let expected_name = format!("42867119_{}", slot); - // - // let file_name = - // TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); - // assert_eq!(&file_name, &expected_name); - // } + { + let slot = 92886; + let expected_name = format!("{}_92900", slot); + let file_name = + TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); + assert_eq!(&file_name, &expected_name); + } + + // finding slot in the middle of the file + { + let first_slot = 92916; + let expected_name = format!("{}_92930", first_slot); + let slot = first_slot + 10; + + let file_name = + TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); + assert_eq!(&file_name, &expected_name); + } + + // finding slot at the end of the file + { + let slot = 92915; + let expected_name = format!("92901_{}", slot); + + let file_name = + TxSetsFileHandler::find_file_by_slot(slot).expect("should return a file"); + assert_eq!(&file_name, &expected_name); + } } #[test] @@ -281,16 +281,15 @@ mod test { } } - // todo: re-enable once a sample of txsetmap is available // ---------------- TEST FOR TXSETs ----------- - // { - // let first_slot = 42867119; - // let find_slot = first_slot + 15; - // let txsets_map = TxSetsFileHandler::get_map_from_archives(find_slot) - // .expect("should return txsets map"); - // - // assert!(txsets_map.get(&find_slot).is_some()); - // } + { + let first_slot = 92901; + let find_slot = first_slot + 15; + let txsets_map = TxSetsFileHandler::get_map_from_archives(find_slot) + .expect("should return txsets map"); + + assert!(txsets_map.get(&find_slot).is_some()); + } } #[test] @@ -307,18 +306,17 @@ mod test { } } - // todo: re-enable once a sample of txsetmap is created // ---------------- TEST FOR TXSETs ----------- - // { - // let slot = 42867087; - // - // match TxSetsFileHandler::get_map_from_archives(slot).expect_err("This should fail") { - // Error::Other(err_str) => { - // assert_eq!(err_str, format!("Cannot find file for slot {}", slot)) - // }, - // _ => assert!(false, "should fail"), - // } - // } + { + let slot = 92931; + + match TxSetsFileHandler::get_map_from_archives(slot).expect_err("This should fail") { + Error::Other(err_str) => { + assert_eq!(err_str, format!("Cannot find file for slot {}", slot)) + }, + _ => assert!(false, "should fail"), + } + } } #[test] @@ -358,38 +356,37 @@ mod test { } // ---------------- TEST FOR TXSETs ----------- - // { - // let first_slot = 42867151; - // let last_slot = 42867166; - // let mut path = PathBuf::new(); - // path.push("./resources/test/tx_sets_for_testing"); - // path.push(&format!("{}_{}", first_slot, last_slot)); - // println!("find file: {:?}", path); - // - // let mut file = File::open(path).expect("file should exist"); - // let mut bytes: Vec = vec![]; - // let _ = file.read_to_end(&mut bytes).expect("should be able to read until the end"); - // - // let mut txset_map = - // TxSetsFileHandler::deserialize_bytes(bytes).expect("should generate a map"); - // - // // let's remove the first_slot and last_slot in the map, so we can create a new file. - // txset_map.remove(&first_slot); - // txset_map.remove(&last_slot); - // - // let expected_filename = format!("{}_{}", first_slot + 1, last_slot - 1); - // let actual_filename = TxSetsFileHandler::write_to_file(&txset_map) - // .expect("should write to scp_envelopes directory"); - // assert_eq!(actual_filename, expected_filename); - // - // let new_file = TxSetsFileHandler::find_file_by_slot(last_slot - 2) - // .expect("should return the same file"); - // assert_eq!(new_file, expected_filename); - // - // // let's delete it - // let path = TxSetsFileHandler::get_path(&new_file); - // fs::remove_file(path).expect("should be able to remove the newly added file."); - // } + { + let first_slot = 92931; + let last_slot = 92945; + let mut path = PathBuf::new(); + path.push("./resources/test/tx_sets_for_testing"); + path.push(&format!("{}_{}", first_slot, last_slot)); + + let mut file = File::open(path).expect("file should exist"); + let mut bytes: Vec = vec![]; + let _ = file.read_to_end(&mut bytes).expect("should be able to read until the end"); + + let mut txset_map = + TxSetsFileHandler::deserialize_bytes(bytes).expect("should generate a map"); + + // let's remove the first_slot and last_slot in the map, so we can create a new file. + txset_map.remove(&first_slot); + txset_map.remove(&last_slot); + + let expected_filename = format!("{}_{}", first_slot + 1, last_slot - 1); + let actual_filename = TxSetsFileHandler::write_to_file(&txset_map) + .expect("should write to scp_envelopes directory"); + assert_eq!(actual_filename, expected_filename); + + let new_file = TxSetsFileHandler::find_file_by_slot(last_slot - 2) + .expect("should return the same file"); + assert_eq!(new_file, expected_filename); + + // let's delete it + let path = TxSetsFileHandler::get_path(&new_file); + fs::remove_file(path).expect("should be able to remove the newly added file."); + } } #[tokio::test] diff --git a/clients/vault/src/oracle/storage/traits.rs b/clients/vault/src/oracle/storage/traits.rs index ec81b5a2e..8baa029b9 100644 --- a/clients/vault/src/oracle/storage/traits.rs +++ b/clients/vault/src/oracle/storage/traits.rs @@ -11,7 +11,8 @@ use sp_core::hexdisplay::AsBytesRef; use stellar_relay_lib::sdk::{compound_types::XdrArchive, XdrCodec}; -use crate::oracle::{constants::ARCHIVE_NODE_LEDGER_BATCH, Error, Filename, SerializedData, Slot}; +use crate::oracle::{constants::ARCHIVE_NODE_LEDGER_BATCH, Error, Filename, SerializedData}; +use wallet::Slot; pub trait FileHandlerExt: FileHandler { fn create_filename_and_data(data: &T) -> Result<(Filename, SerializedData), Error>; diff --git a/clients/vault/src/oracle/testing_utils.rs b/clients/vault/src/oracle/testing_utils.rs index 6f245cd6e..bb464d4a3 100644 --- a/clients/vault/src/oracle/testing_utils.rs +++ b/clients/vault/src/oracle/testing_utils.rs @@ -43,9 +43,11 @@ fn stellar_relay_config_abs_path( .expect("should be able to extract config") } -pub fn get_test_secret_key(is_mainnet: bool) -> String { - let file_name = if is_mainnet { "mainnet" } else { "testnet" }; - let path = format!("./resources/secretkey/stellar_secretkey_{file_name}"); +pub fn get_secret_key(with_currency: bool, is_mainnet: bool) -> String { + let suffix = if with_currency { "_with_currency" } else { "" }; + let directory = if is_mainnet { "mainnet" } else { "testnet" }; + + let path = format!("./resources/secretkey/{directory}/stellar_secretkey_{directory}{suffix}"); std::fs::read_to_string(path).expect("should return a string") } diff --git a/clients/vault/src/oracle/types/constants.rs b/clients/vault/src/oracle/types/constants.rs index eff1ea20f..63edb748a 100644 --- a/clients/vault/src/oracle/types/constants.rs +++ b/clients/vault/src/oracle/types/constants.rs @@ -1,4 +1,4 @@ -use crate::oracle::types::Slot; +use wallet::Slot; /// This is for `EnvelopesMap`; how many slots is accommodated per file. /// This is used to compare against the length of the "keys", diff --git a/clients/vault/src/oracle/types/double_sided_map.rs b/clients/vault/src/oracle/types/double_sided_map.rs index f3ed26b34..a0647b342 100644 --- a/clients/vault/src/oracle/types/double_sided_map.rs +++ b/clients/vault/src/oracle/types/double_sided_map.rs @@ -1,7 +1,8 @@ #![allow(non_snake_case)] -use crate::oracle::types::{Slot, TxSetHash}; +use crate::oracle::types::TxSetHash; use std::collections::HashMap; +use wallet::Slot; /// The slot is not found in the `StellarMessage::TxSet(...)` and /// `StellarMessage::GeneralizedTxSet(...)`, therefore this map serves as a holder of the slot when diff --git a/clients/vault/src/oracle/types/limited_fifo_map.rs b/clients/vault/src/oracle/types/limited_fifo_map.rs index 9dd179529..baee598f0 100644 --- a/clients/vault/src/oracle/types/limited_fifo_map.rs +++ b/clients/vault/src/oracle/types/limited_fifo_map.rs @@ -1,9 +1,10 @@ #![allow(non_snake_case)] -use crate::oracle::{constants::DEFAULT_MAX_ITEMS_IN_QUEUE, types::Slot}; +use crate::oracle::constants::DEFAULT_MAX_ITEMS_IN_QUEUE; use itertools::Itertools; use std::{collections::VecDeque, fmt::Debug}; use stellar_relay_lib::sdk::{types::ScpEnvelope, TransactionSetType}; +use wallet::Slot; /// Sometimes not enough `StellarMessage::ScpMessage(...)` are sent per slot; /// or that the `StellarMessage::TxSet(...)` or `StellarMessage::GeneralizedTxSet(...)` @@ -68,6 +69,12 @@ where let (index, _) = self.queue.iter().find_position(|(k, _)| k == key)?; self.queue.remove(index).map(|(_, v)| v) } + + /// removes all data in the queue + pub fn clear(&mut self) { + self.queue.drain(..); + } + pub fn get(&self, key: &K) -> Option<&T> { self.queue.iter().find(|(k, _)| k == key).map(|(_, v)| v) } diff --git a/clients/vault/src/oracle/types/types.rs b/clients/vault/src/oracle/types/types.rs index 5424a2060..ada70f066 100644 --- a/clients/vault/src/oracle/types/types.rs +++ b/clients/vault/src/oracle/types/types.rs @@ -4,9 +4,9 @@ use std::collections::BTreeMap; use tokio::sync::mpsc; -use stellar_relay_lib::sdk::types::{Hash, StellarMessage, Uint64}; +use stellar_relay_lib::sdk::types::{Hash, StellarMessage}; +use wallet::Slot; -pub type Slot = Uint64; pub type TxHash = Hash; pub type TxSetHash = Hash; pub type Filename = String; diff --git a/clients/vault/src/requests/execution.rs b/clients/vault/src/requests/execution.rs index 8d0c0ab95..0c719ce52 100644 --- a/clients/vault/src/requests/execution.rs +++ b/clients/vault/src/requests/execution.rs @@ -1,6 +1,6 @@ use crate::{ error::Error, - oracle::{types::Slot, OracleAgent}, + oracle::OracleAgent, requests::{ helper::{ get_all_transactions_of_wallet_async, get_request_for_stellar_tx, @@ -23,7 +23,7 @@ use runtime::{PrettyPrint, ShutdownSender, SpacewalkParachain, UtilFuncs}; use service::{spawn_cancelable, Error as ServiceError}; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::RwLock; -use wallet::{StellarWallet, TransactionResponse}; +use wallet::{Slot, StellarWallet, TransactionResponse}; // max of 3 retries for failed request execution const MAX_EXECUTION_RETRIES: u32 = 3; diff --git a/clients/vault/src/requests/structs.rs b/clients/vault/src/requests/structs.rs index cb94f1e30..edf190bf8 100644 --- a/clients/vault/src/requests/structs.rs +++ b/clients/vault/src/requests/structs.rs @@ -1,6 +1,6 @@ use crate::{ metrics::update_stellar_metrics, - oracle::{types::Slot, OracleAgent, Proof}, + oracle::{OracleAgent, Proof}, system::VaultData, Error, }; @@ -14,7 +14,7 @@ use sp_runtime::traits::StaticLookup; use std::{convert::TryInto, sync::Arc, time::Duration}; use stellar_relay_lib::sdk::{Asset, TransactionEnvelope, XdrCodec}; use tokio::sync::RwLock; -use wallet::{StellarWallet, TransactionResponse}; +use wallet::{Slot, StellarWallet, TransactionResponse}; #[derive(Debug, Clone, PartialEq)] struct Deadline { diff --git a/clients/vault/src/system.rs b/clients/vault/src/system.rs index dca8a19ae..1a61238d3 100644 --- a/clients/vault/src/system.rs +++ b/clients/vault/src/system.rs @@ -440,11 +440,18 @@ impl VaultService { oracle_agent, self.config.payment_margin_minutes, ); + + let shutdown_clone = self.shutdown.clone(); service::spawn_cancelable(self.shutdown.subscribe(), async move { - // TODO: kill task on shutdown signal to prevent double payment - if let Err(e) = open_request_executor.await { - tracing::error!("Failed to process open requests: {}", e) - }; + match open_request_executor.await { + Ok(_) => tracing::info!("Done processing open requests"), + Err(e) => { + tracing::error!("Failed to process open requests: {}", e); + if let Err(err) = shutdown_clone.send(()) { + tracing::error!("Failed to send shutdown signal: {}", err); + } + }, + } }); } @@ -708,15 +715,7 @@ impl VaultService { monitoring_config: MonitoringConfig, shutdown: ShutdownSender, ) -> Result { - let is_public_network = - spacewalk_parachain.is_public_network().await.unwrap_or_else(|error| { - // Sometimes the fetch fails with 'StorageItemNotFound' error. - // We assume public network by default - tracing::warn!( - "Failed to fetch public network status from parachain: {error}. Assuming public network." - ); - true - }); + let is_public_network = spacewalk_parachain.is_public_network().await; let secret_key = fs::read_to_string(&config.stellar_vault_secret_key_filepath)? .trim() @@ -822,9 +821,7 @@ impl VaultService { } async fn register_public_key_if_not_present(&mut self) -> Result<(), Error> { - if let Some(_faucet_url) = &self.config.faucet_url { - // TODO fund account with faucet - } + let _ = self.try_fund_from_faucet().await; if self.spacewalk_parachain.get_public_key().await?.is_none() { let public_key = self.stellar_wallet.read().await.public_key(); @@ -841,6 +838,37 @@ impl VaultService { Ok(()) } + /// Only works when the stellar network is testnet + async fn try_fund_from_faucet(&self) -> bool { + let Some(faucet_url) = &self.config.faucet_url else { + return false + }; + + let is_public_network = self.spacewalk_parachain.is_public_network().await; + + // fund the account if on stellar TESTNET + if !is_public_network { + let account_id = self.spacewalk_parachain.get_account_id().pretty_print(); + let url = format!("{faucet_url}?to={account_id}"); + match reqwest::get(url.clone()).await { + Ok(response) if response.status().is_success() => { + tracing::info!("try_fund_from_faucet(): successful funded {account_id}"); + return true + }, + Ok(response) => { + tracing::error!("try_fund_from_faucet(): failed to fund {account_id} from faucet: {response:#?}"); + }, + Err(e) => { + tracing::error!( + "try_fund_from_faucet(): failed to fund {account_id} from faucet: {e}" + ); + }, + } + } + + false + } + async fn register_vault_with_collateral( &self, vault_id: VaultId, @@ -869,11 +897,7 @@ impl VaultService { ) .await .map_err(|e| Error::RuntimeError(e)) - } else if let Some(_faucet_url) = &self.config.faucet_url { - tracing::info!("[{}] Automatically registering...", vault_id.pretty_print()); - // TODO - // faucet::fund_and_register(&self.spacewalk_parachain, faucet_url, &vault_id) - // .await?; + } else if self.try_fund_from_faucet().await { Ok(()) } else { tracing::error!( diff --git a/clients/vault/tests/helper/helper.rs b/clients/vault/tests/helper/helper.rs index e2a4ae512..740160b32 100644 --- a/clients/vault/tests/helper/helper.rs +++ b/clients/vault/tests/helper/helper.rs @@ -9,13 +9,12 @@ use runtime::{ integration::{ assert_event, get_required_vault_collateral_for_issue, setup_provider, SubxtClient, }, - stellar::SecretKey, ExecuteRedeemEvent, IssuePallet, SpacewalkParachain, VaultId, VaultRegistryPallet, }; use sp_keyring::AccountKeyring; use sp_runtime::traits::StaticLookup; use std::{sync::Arc, time::Duration}; -use stellar_relay_lib::sdk::PublicKey; +use stellar_relay_lib::sdk::{PublicKey, SecretKey}; use vault::{oracle::OracleAgent, ArcRwLock}; use wallet::{error::Error, StellarWallet, TransactionResponse}; diff --git a/clients/vault/tests/helper/mod.rs b/clients/vault/tests/helper/mod.rs index 3e3a3bce0..1177c1b29 100644 --- a/clients/vault/tests/helper/mod.rs +++ b/clients/vault/tests/helper/mod.rs @@ -20,7 +20,7 @@ use std::{future::Future, sync::Arc}; use stellar_relay_lib::StellarOverlayConfig; use tokio::sync::RwLock; use vault::{ - oracle::{get_test_secret_key, random_stellar_relay_config, start_oracle_agent, OracleAgent}, + oracle::{get_secret_key, random_stellar_relay_config, start_oracle_agent, OracleAgent}, ArcRwLock, }; use wallet::StellarWallet; @@ -31,8 +31,9 @@ pub type StellarPublicKey = [u8; 32]; impl SpacewalkParachainExt for SpacewalkParachain {} lazy_static! { - // TODO clean this up by extending the `get_test_secret_key()` function - pub static ref DESTINATION_SECRET_KEY: String = "SDNQJEIRSA6YF5JNS6LQLCBF2XVWZ2NJV3YLC322RGIBJIJRIRGWKLEF".to_string(); + pub static ref CFG: StellarOverlayConfig = random_stellar_relay_config(false); + pub static ref SECRET_KEY: String = get_secret_key(true, false); + pub static ref DESTINATION_SECRET_KEY: String = get_secret_key(false, false); pub static ref ONE_TO_ONE_RATIO: FixedU128 = FixedU128::saturating_from_rational(1u128, 1u128); pub static ref TEN_TO_ONE_RATIO: FixedU128 = FixedU128::saturating_from_rational(1u128, 10u128); } @@ -84,8 +85,7 @@ async fn setup_chain_providers( let path = tmp_dir.path().to_str().expect("should return a string").to_string(); let stellar_config = random_stellar_relay_config(is_public_network); - let vault_stellar_secret = get_test_secret_key(is_public_network); - // TODO set destination secret key in a better way + let vault_stellar_secret = get_secret_key(true, is_public_network); let user_stellar_secret = &DESTINATION_SECRET_KEY; let (vault_wallet, user_wallet) = @@ -139,7 +139,7 @@ where ); let stellar_config = random_stellar_relay_config(is_public_network); - let vault_stellar_secret = get_test_secret_key(is_public_network); + let vault_stellar_secret = get_secret_key(true, is_public_network); let shutdown_tx = ShutdownSender::new(); let oracle_agent = diff --git a/clients/vault/tests/vault_integration_tests.rs b/clients/vault/tests/vault_integration_tests.rs index 05c7db084..98df7a4c9 100644 --- a/clients/vault/tests/vault_integration_tests.rs +++ b/clients/vault/tests/vault_integration_tests.rs @@ -23,7 +23,7 @@ mod helper; use helper::*; use primitives::DecimalsLookup; -use vault::oracle::{get_test_secret_key, random_stellar_relay_config, start_oracle_agent}; +use vault::oracle::{get_secret_key, random_stellar_relay_config, start_oracle_agent}; #[tokio::test(flavor = "multi_thread")] #[serial] @@ -662,7 +662,7 @@ async fn test_issue_execution_succeeds_from_archive() { let shutdown_tx = ShutdownSender::new(); let stellar_config = random_stellar_relay_config(is_public_network); - let vault_stellar_secret = get_test_secret_key(is_public_network); + let vault_stellar_secret = get_secret_key(true, is_public_network); // Create new oracle agent with the same configuration as the previous one let oracle_agent = start_oracle_agent(stellar_config.clone(), &vault_stellar_secret, shutdown_tx) diff --git a/clients/wallet/src/horizon/horizon.rs b/clients/wallet/src/horizon/horizon.rs index c275dc69c..3437f08eb 100644 --- a/clients/wallet/src/horizon/horizon.rs +++ b/clients/wallet/src/horizon/horizon.rs @@ -129,11 +129,11 @@ impl HorizonClient for reqwest::Client { ) -> Result { let seq_no = transaction_envelope.sequence_number(); - tracing::debug!("submitting transaction with seq no: {seq_no:?}: {transaction_envelope:?}"); - let transaction_xdr = transaction_envelope.to_base64_xdr(); let transaction_xdr = std::str::from_utf8(&transaction_xdr).map_err(Error::Utf8Error)?; + tracing::debug!("submit_transaction(): with seq no: {seq_no:?}: {transaction_xdr:?}"); + let params = [("tx", &transaction_xdr)]; let mut server_error_count = 0; diff --git a/clients/wallet/src/horizon/mod.rs b/clients/wallet/src/horizon/mod.rs index 5004d94a6..388fdb9b8 100644 --- a/clients/wallet/src/horizon/mod.rs +++ b/clients/wallet/src/horizon/mod.rs @@ -8,6 +8,3 @@ mod tests; pub use horizon::*; pub use traits::HorizonClient; - -// todo: change to Slot -pub type Ledger = u32; diff --git a/clients/wallet/src/horizon/responses.rs b/clients/wallet/src/horizon/responses.rs index eaf7e16d1..0eaef11cb 100644 --- a/clients/wallet/src/horizon/responses.rs +++ b/clients/wallet/src/horizon/responses.rs @@ -1,7 +1,8 @@ use crate::{ error::Error, - horizon::{serde::*, traits::HorizonClient, Ledger}, + horizon::{serde::*, traits::HorizonClient}, types::{FeeAttribute, PagingToken, StatusCode}, + Slot, }; use parity_scale_codec::{Decode, Encode}; use primitives::{ @@ -54,10 +55,11 @@ pub(crate) async fn interpret_response( .await .map_err(Error::HorizonResponseError)?; - let title = resp[RESPONSE_FIELD_TITLE].as_str().unwrap_or(VALUE_UNKNOWN); let status = StatusCode::try_from(resp[RESPONSE_FIELD_STATUS].as_u64().unwrap_or(400)).unwrap_or(400); + let title = resp[RESPONSE_FIELD_TITLE].as_str().unwrap_or(VALUE_UNKNOWN); + let error = match status { 400 => { let envelope_xdr = resp[RESPONSE_FIELD_EXTRAS][RESPONSE_FIELD_ENVELOPE_XDR] @@ -168,7 +170,7 @@ pub struct TransactionResponse { pub successful: bool, #[serde(deserialize_with = "de_string_to_bytes")] pub hash: Vec, - pub ledger: Ledger, + pub ledger: Slot, #[serde(deserialize_with = "de_string_to_bytes")] pub created_at: Vec, #[serde(deserialize_with = "de_string_to_bytes")] @@ -230,7 +232,7 @@ impl Debug for TransactionResponse { #[allow(dead_code)] impl TransactionResponse { - pub(crate) fn ledger(&self) -> Ledger { + pub(crate) fn ledger(&self) -> Slot { self.ledger } @@ -390,8 +392,8 @@ pub struct FeeDistribution { #[derive(Deserialize, Debug)] pub struct FeeStats { - #[serde(deserialize_with = "de_string_to_u32")] - pub last_ledger: Ledger, + #[serde(deserialize_with = "de_string_to_u64")] + pub last_ledger: Slot, #[serde(deserialize_with = "de_string_to_u32")] pub last_ledger_base_fee: u32, #[serde(deserialize_with = "de_string_to_f64")] @@ -436,7 +438,7 @@ pub struct ClaimableBalance { pub sponsor: Vec, pub claimants: Vec, - pub last_modified_ledger: Ledger, + pub last_modified_ledger: Slot, #[serde(deserialize_with = "de_string_to_bytes")] pub last_modified_time: Vec, } @@ -471,7 +473,7 @@ impl TransactionsResponseIter { } #[doc(hidden)] - // returns the first record of the list + // returns the first transaction response of the list fn get_top_record(&mut self) -> Option { if !self.is_empty() { return Some(self.records.remove(0)) @@ -479,21 +481,51 @@ impl TransactionsResponseIter { None } - /// returns the next TransactionResponse in the list + /// returns the next TransactionResponse pub async fn next(&mut self) -> Option { match self.get_top_record() { Some(record) => Some(record), None => { - // call the next page - tracing::trace!("calling next page: {}", &self.next_page); - - let response: HorizonTransactionsResponse = - self.client.get_from_url(&self.next_page).await.ok()?; - self.next_page = response.next_page(); - self.records = response.records(); - + let _ = self.jump_to_next_page().await?; self.get_top_record() }, } } + + /// returns the next TransactionResponse in reverse order + pub fn next_back(&mut self) -> Option { + self.records.pop() + } + + /// returns the TransactionResponse in the middle of the list + pub fn middle(&mut self) -> Option { + if !self.is_empty() { + let idx = self.records.len() / 2; + return Some(self.records.remove(idx)) + } + None + } + + pub fn remove_last_half_records(&mut self) { + let idx = self.records.len() / 2; + if idx != 0 { + self.records = self.records[..idx].to_vec(); + } + } + + pub fn remove_first_half_records(&mut self) { + let idx = self.records.len() / 2; + if idx != 0 { + self.records = self.records[idx..].to_vec(); + } + } + + pub async fn jump_to_next_page(&mut self) -> Option<()> { + let response: HorizonTransactionsResponse = + self.client.get_from_url(&self.next_page).await.ok()?; + self.next_page = response.next_page(); + self.records = response.records(); + + Some(()) + } } diff --git a/clients/wallet/src/horizon/tests.rs b/clients/wallet/src/horizon/tests.rs index e963ab197..09d0d3265 100644 --- a/clients/wallet/src/horizon/tests.rs +++ b/clients/wallet/src/horizon/tests.rs @@ -175,17 +175,22 @@ async fn fetch_transactions_iter_success() { let mut txs_iter = fetcher.fetch_transactions_iter(0).await.expect("should return a response"); - let next_page = txs_iter.next_page.clone(); - assert!(!next_page.is_empty()); - for _ in 0..txs_iter.records.len() { assert!(txs_iter.next().await.is_some()); } - // the list should be empty, as the last record was returned. + // the list should be empty, as the last record of this page was returned. assert_eq!(txs_iter.records.len(), 0); - // todo: when this account's # of transactions is more than 200, add a test case for it. + // if the next page + let next_page = txs_iter.next_page.clone(); + if !next_page.is_empty() { + // continue reading for transactions + assert!(txs_iter.next().await.is_some()); + + // new records can be read + assert_ne!(txs_iter.records.len(), 0); + } } #[tokio::test(flavor = "multi_thread")] diff --git a/clients/wallet/src/resubmissions.rs b/clients/wallet/src/resubmissions.rs index 9a880edd7..db4d8f1e4 100644 --- a/clients/wallet/src/resubmissions.rs +++ b/clients/wallet/src/resubmissions.rs @@ -14,13 +14,14 @@ use primitives::{ use std::time::Duration; use tokio::time::sleep; +use crate::horizon::responses::TransactionsResponseIter; #[cfg(test)] use mocktopus::macros::mockable; +use primitives::stellar::{types::SequenceNumber, PublicKey}; +use reqwest::Client; pub const RESUBMISSION_INTERVAL_IN_SECS: u64 = 1800; -const MAX_LOOK_BACK_PAGES: u8 = 10; - #[cfg_attr(test, mockable)] impl StellarWallet { pub async fn start_periodic_resubmission_of_transactions_from_cache( @@ -37,7 +38,6 @@ impl StellarWallet { tokio::spawn(async move { let me_clone = Arc::clone(&me); loop { - // Loops every 30 minutes or 1800 seconds pause_process_in_secs(interval_in_seconds).await; me_clone._resubmit_transactions_from_cache().await; @@ -193,6 +193,143 @@ impl StellarWallet { } } +fn is_source_account_match(public_key: &PublicKey, tx: &TransactionResponse) -> bool { + match tx.source_account() { + Err(_) => false, + Ok(source_account) if !source_account.eq(&public_key) => false, + _ => true, + } +} + +fn is_memo_match(tx1: &Transaction, tx2: &TransactionResponse) -> bool { + if let Some(response_memo) = tx2.memo_text() { + let Memo::MemoText(tx_memo) = &tx1.memo else { + return false + }; + + if are_memos_eq(response_memo, tx_memo.get_vec()) { + return true + } + } + false +} + +#[doc(hidden)] +/// A helper function which returns: +/// Ok(true) if both transactions match; +/// Ok(false) if the source account and the sequence number match, but NOT the MEMO; +/// Err(None) if it's absolutely NOT a match +/// Err(Some(SequenceNumber)) if the sequence number can be used for further logic checking +/// +/// # Arguments +/// +/// * `tx` - the transaction we want to confirm if it's already submitted +/// * `tx_resp` - the transaction response from Horizon +/// * `public_key` - the public key of the wallet +fn _check_transaction_match( + tx: &Transaction, + tx_resp: &TransactionResponse, + public_key: &PublicKey, +) -> Result> { + // Make sure that we are the sender and not the receiver because otherwise an + // attacker could send a transaction to us with the target memo and we'd wrongly + // assume that we already submitted this transaction. + if !is_source_account_match(public_key, &tx_resp) { + return Err(None) + } + + let Ok(source_account_sequence) = tx_resp.source_account_sequence() else { + tracing::warn!("_check_transaction_match(): cannot extract sequence number of transaction response: {tx_resp:?}"); + return Err(None) + }; + + // check if the sequence number is the same as this response + if tx.seq_num == source_account_sequence { + // Check if the transaction contains the memo we want to send + return Ok(is_memo_match(tx, &tx_resp)) + } + + Err(Some(source_account_sequence)) +} + +#[doc(hidden)] +/// A helper function which returns: +/// true if the transaction in the MIDDLE of the list is a match; +/// false if NOT a match; +/// None if a match can be found else where by narrowing down the search: +/// * if the sequence number of the transaction is < than what we're looking for, then update the +/// iterator by removing the LAST half of the list; +/// * else remove the FIRST half of the list +/// +/// # Arguments +/// +/// * `iter` - the iterator to iterate over a list of `TransactionResponse` +/// * `tx` - the transaction we want to confirm if it's already submitted +/// * `public_key` - the public key of the wallet +fn check_middle_transaction_match( + iter: &mut TransactionsResponseIter, + tx: &Transaction, + public_key: &PublicKey, +) -> Option { + let tx_sequence_num = tx.seq_num; + + let Some(response) = iter.middle() else { + return None + }; + + match _check_transaction_match(tx, &response, public_key) { + Ok(res) => return Some(res), + Err(Some(source_account_sequence)) => { + // if the sequence number is GREATER than this response, + // then a match must be in the first half of the list. + if tx_sequence_num > source_account_sequence { + iter.remove_last_half_records(); + } + // a match must be in the last half of the list. + else { + iter.remove_first_half_records(); + } + }, + _ => {}, + } + None +} + +#[doc(hidden)] +/// A helper function which returns: +/// true if the LAST transaction of the list is a match; +/// false if NOT a match; +/// None if a match can be found else where: +/// * if the sequence number of the transaction is > than what we're looking for, then update the +/// iterator by jumping to the next page; +/// * if there's no next page, then a match will never be found. Return FALSE. +async fn check_last_transaction_match( + iter: &mut TransactionsResponseIter, + tx: &Transaction, + public_key: &PublicKey, +) -> Option { + let tx_sequence_num = tx.seq_num; + let Some(response) = iter.next_back() else { + return None + }; + + match _check_transaction_match(tx, &response, public_key) { + Ok(res) => return Some(res), + Err(Some(source_account_sequence)) => { + // if the sequence number is LESSER than this response, + // then a match is possible on the NEXT page + if tx_sequence_num < source_account_sequence { + if let None = iter.jump_to_next_page().await { + // there's no pages left, meaning there's no other transactions to compare + return Some(false) + } + } + }, + _ => {}, + } + None +} + // handle tx_bad_seq #[cfg_attr(test, mockable)] impl StellarWallet { @@ -245,48 +382,54 @@ impl StellarWallet { self.submit_transaction(envelope).await } - /// This function iterates over all transactions of an account to see if a similar transaction - /// i.e. a transaction containing the same memo was already submitted previously. - /// TODO: This operation is very costly and we should try to optimize it in the future. + /// returns true if a transaction already exists and WAS submitted successfully. async fn is_transaction_already_submitted(&self, tx: &Transaction) -> bool { - // loop through until the 10th page - let mut remaining_page = 0; + let tx_sequence_num = tx.seq_num; let own_public_key = self.public_key(); - while let Ok(transaction) = self.get_all_transactions_iter().await { - if remaining_page == MAX_LOOK_BACK_PAGES { + // get the iterator + let mut iter = match self.get_all_transactions_iter().await { + Ok(iter) => iter, + Err(e) => { + tracing::warn!("is_transaction_already_submitted(): failed to get iterator: {e:?}"); + return false + }, + }; + + // iterate over the transactions, starting from + // the TOP (the largest sequence number/the latest transaction) + while let Some(response) = iter.next().await { + let top_sequence_num = match _check_transaction_match(tx, &response, &own_public_key) { + // return result for partial match + Ok(res) => return res, + // continue if it is absolutely not a match + Err(None) => continue, + // further logic checking required + Err(Some(seq_num)) => seq_num, + }; + + // if the sequence number is GREATER than this response, + // no other transaction will ever match with it. + if tx_sequence_num > top_sequence_num { break } - for response in transaction.records { - // Make sure that we are the sender and not the receiver because otherwise an - // attacker could send a transaction to us with the target memo and we'd wrongly - // assume that we already submitted this transaction. - match response.source_account() { - // no source account was found; move on to the next response - Err(_) => continue, - // the wallet's public key is not this response's source account; - // move on to the next response - Ok(source_account) if !source_account.eq(&own_public_key) => continue, - _ => {}, - } - - // Check that the transaction contains the memo that we want to send. - if let Some(response_memo) = response.memo_text() { - let Memo::MemoText(tx_memo) = &tx.memo else { - continue - }; + // check the middle response OR remove half of the responses that won't match. + if let Some(result) = check_middle_transaction_match(&mut iter, tx, &own_public_key) { + // if the middle response matched (both source account and sequence number), + // return that result + return result + } - if are_memos_eq(response_memo, tx_memo.get_vec()) { - return true - } - } + // if no match was found, check the last response OR jump to the next page + if let Some(result) = check_last_transaction_match(&mut iter, tx, &own_public_key).await + { + return result } - remaining_page += 1; + // if no match was found, continue to the next response } - // We did not find a transaction that matched our criteria false } } @@ -328,7 +471,7 @@ mod test { #[tokio::test] #[serial] async fn check_is_transaction_already_submitted() { - let wallet = wallet_with_storage("resources/check_is_transaction_already_submitted") + let wallet = wallet_with_storage("resources/checkcheck_middle_transaction_match") .expect("") .clone(); let mut wallet = wallet.write().await; @@ -357,6 +500,17 @@ mod test { assert!(wallet.is_transaction_already_submitted(&tx).await); } + // test is_transaction_already_submitted when transaction is on the next pages + { + // sequence number: 1017907249295 + let envelope = "AAAAAgAAAACI3DQX1QWOxLRQPgwS6hoKib4gD+mJIkI9QzQBT6aw7gAAAGQAAADtAAAAjwAAAAAAAAABAAAAHDExMTExMTExMTExMTExMTExMTExMTExMTExMTEAAAABAAAAAQAAAACI3DQX1QWOxLRQPgwS6hoKib4gD+mJIkI9QzQBT6aw7gAAAAEAAAAAO5kROA7+mIugqJAOsc/kTzZvfb6Ua+0HckD39iTfFcUAAAAAAAAAAAAAA+gAAAAAAAAAAU+msO4AAABAMVQZONg4CsTjVe7nmrY2LX86a7VWrmv8uL37zkqwY9Qpxte/76pUnZ/hN8o7EkpzBAWr5qb85cvzAlPgbQVGCA=="; + let tx_env = TransactionEnvelope::from_base64_xdr(envelope) + .expect("should decode into transactionenvelope"); + let tx = tx_env.get_transaction().expect("should return a transaction"); + + assert!(wallet.is_transaction_already_submitted(&tx).await); + } + // test is_transaction_already_submitted returns false { let dummy_tx = create_basic_spacewalk_stellar_transaction( @@ -401,12 +555,18 @@ mod test { let dummy_transaction = dummy_envelope.get_transaction().expect("must return a transaction"); - let _ = wallet + let resp = wallet .bump_sequence_number_and_submit(dummy_transaction.clone()) .await .expect("return ok"); + let new_dummy_transaction = + String::from_utf8(resp.envelope_xdr).expect("should return a String"); + let new_dummy_env = TransactionEnvelope::from_base64_xdr(new_dummy_transaction) + .expect("should return an envelope"); + let new_dummy_transaction = + new_dummy_env.get_transaction().expect("should return a transaction"); - assert!(wallet.is_transaction_already_submitted(&dummy_transaction).await); + assert!(wallet.is_transaction_already_submitted(&new_dummy_transaction).await); } // test bump_sequence_number_and_submit failed diff --git a/clients/wallet/src/types.rs b/clients/wallet/src/types.rs index 1f7e7a2ed..cadf11bb4 100644 --- a/clients/wallet/src/types.rs +++ b/clients/wallet/src/types.rs @@ -3,7 +3,7 @@ use primitives::stellar::TransactionEnvelope; use std::{collections::HashMap, fmt, fmt::Formatter}; pub type PagingToken = u128; -pub type Slot = u32; +pub type Slot = u64; pub type StatusCode = u16; pub type LedgerTxEnvMap = HashMap; diff --git a/pallets/oracle/src/lib.rs b/pallets/oracle/src/lib.rs index f52409c22..22fe14e0f 100644 --- a/pallets/oracle/src/lib.rs +++ b/pallets/oracle/src/lib.rs @@ -418,8 +418,7 @@ impl Pallet { >::get() } - /// TODO - /// Set the current exchange rate. ONLY FOR TESTING. + /// Set the current exchange rate. /// /// # Arguments /// diff --git a/pallets/oracle/src/tests.rs b/pallets/oracle/src/tests.rs index 8d0f6cf1a..7a12a8a5d 100644 --- a/pallets/oracle/src/tests.rs +++ b/pallets/oracle/src/tests.rs @@ -301,13 +301,6 @@ fn test_is_invalidated() { assert_ok!(Oracle::feed_values(3, vec![(key.clone(), rate)])); mine_block(); - - // max delay is 60 minutes, 60+ passed - // assert!(Oracle::is_outdated(&key, now + 3601));//TODO - - // max delay is 60 minutes, 30 passed - Oracle::get_current_time.mock_safe(move || MockResult::Return(now + 1800)); - // assert!(!Oracle::is_outdated(&key, now + 3599)); //TODO }); } diff --git a/pallets/redeem/src/benchmarking.rs b/pallets/redeem/src/benchmarking.rs index 23995ba43..3ca45806a 100644 --- a/pallets/redeem/src/benchmarking.rs +++ b/pallets/redeem/src/benchmarking.rs @@ -62,22 +62,19 @@ fn mint_wrapped(account_id: &T::AccountId, amount: BalanceOf() { let oracle_id: T::AccountId = account("Oracle", 12, 0); - use primitives::oracle::Key; - let result = Oracle::::feed_values( + Oracle::::_set_exchange_rate( + oracle_id.clone(), + get_collateral_currency_id::(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); + + Oracle::::_set_exchange_rate( oracle_id, - vec![ - ( - Key::ExchangeRate(get_collateral_currency_id::()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ( - Key::ExchangeRate(get_wrapped_currency_id()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ], - ); - assert_ok!(result); - Oracle::::begin_block(0u32.into()); + get_wrapped_currency_id(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); } fn test_request(vault_id: &DefaultVaultId) -> DefaultRedeemRequest { @@ -112,6 +109,7 @@ benchmarks! { let asset = CurrencyId::XCM(0); let stellar_address = DEFAULT_STELLAR_PUBLIC_KEY; + Security::::set_active_block_number(1u32.into()); initialize_oracle::(); register_public_key::(vault_id.clone()); @@ -135,12 +133,17 @@ benchmarks! { }: _(RawOrigin::Signed(origin), amount, stellar_address, vault_id) liquidation_redeem { - initialize_oracle::(); - let origin: T::AccountId = account("Origin", 0, 0); let vault_id = get_vault_id::(); let amount = 1000; + mint_wrapped::(&origin, amount.into()); + + mint_collateral::(&vault_id.account_id, 100_000u32.into()); + + Security::::set_active_block_number(1u32.into()); + initialize_oracle::(); + register_public_key::(vault_id.clone()); VaultRegistry::::insert_vault( @@ -148,9 +151,6 @@ benchmarks! { Vault::new(vault_id.clone()) ); - mint_wrapped::(&origin, amount.into()); - - mint_collateral::(&vault_id.account_id, 100_000u32.into()); assert_ok!(VaultRegistry::::try_deposit_collateral(&vault_id, &collateral(100_000))); assert_ok!(VaultRegistry::::try_increase_to_be_issued_tokens(&vault_id, &wrapped(amount))); @@ -168,6 +168,8 @@ benchmarks! { let vault_id = get_vault_id::(); let relayer_id: T::AccountId = account("Relayer", 0, 0); + + Security::::set_active_block_number(1u32.into()); initialize_oracle::(); let origin_stellar_address = DEFAULT_STELLAR_PUBLIC_KEY; @@ -189,8 +191,6 @@ benchmarks! { vault ); - Security::::set_active_block_number(1u32.into()); - let (validators, organizations) = get_validators_and_organizations::(); let enactment_block_height = T::BlockNumber::default(); StellarRelay::::_update_tier_1_validator_set(validators, organizations, enactment_block_height).unwrap(); @@ -302,8 +302,6 @@ benchmarks! { let vault_id = get_vault_id::(); let relayer_id: T::AccountId = account("Relayer", 0, 0); - initialize_oracle::(); - let origin_stellar_address = DEFAULT_STELLAR_PUBLIC_KEY; let redeem_id = H256::zero(); diff --git a/pallets/replace/src/benchmarking.rs b/pallets/replace/src/benchmarking.rs index 25178cdd8..d960d53f1 100644 --- a/pallets/replace/src/benchmarking.rs +++ b/pallets/replace/src/benchmarking.rs @@ -10,7 +10,7 @@ use currency::{ getters::{get_relay_chain_currency_id as get_collateral_currency_id, *}, testing_constants::get_wrapped_currency_id, }; -use oracle::{OracleKey, Pallet as Oracle}; +use oracle::Pallet as Oracle; use primitives::{CurrencyId, VaultCurrencyPair, VaultId}; use security::Pallet as Security; use stellar_relay::{ @@ -21,7 +21,7 @@ use stellar_relay::{ Config as StellarRelayConfig, Pallet as StellarRelay, }; use vault_registry::{ - types::{DefaultVaultCurrencyPair, Vault}, + types::{DefaultVault, DefaultVaultCurrencyPair, Vault}, Pallet as VaultRegistry, }; @@ -59,25 +59,26 @@ fn mint_collateral(account_id: &T::AccountId, amount: BalanceO fn initialize_oracle() { let oracle_id: T::AccountId = account("Oracle", 12, 0); - let result = Oracle::::feed_values( + Oracle::::_set_exchange_rate( + oracle_id.clone(), + get_collateral_currency_id::(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); + + Oracle::::_set_exchange_rate( + oracle_id.clone(), + get_native_currency_id::(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); + + Oracle::::_set_exchange_rate( oracle_id, - vec![ - ( - OracleKey::ExchangeRate(get_collateral_currency_id::()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ( - OracleKey::ExchangeRate(get_native_currency_id::()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ( - OracleKey::ExchangeRate(get_wrapped_currency_id()), - UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), - ), - ], - ); - assert_ok!(result); - Oracle::::begin_block(0u32.into()); + get_wrapped_currency_id(), + UnsignedFixedPoint::::checked_from_rational(1, 1).unwrap(), + ) + .unwrap(); } fn test_request( @@ -114,11 +115,13 @@ fn register_vault(vault_id: DefaultVaultId) { benchmarks! { request_replace { - initialize_oracle::(); let vault_id = get_vault_id::("Vault"); mint_collateral::(&vault_id.account_id, (1u32 << 31).into()); let amount = Replace::::minimum_transfer_amount(get_wrapped_currency_id()).amount() + 1000_0000u32.into(); + Security::::set_active_block_number(1u32.into()); + + initialize_oracle::(); register_public_key::(vault_id.clone()); let vault = Vault { @@ -136,11 +139,13 @@ benchmarks! { }: _(RawOrigin::Signed(vault_id.account_id.clone()), vault_id.currencies.clone(), amount) withdraw_replace { - initialize_oracle::(); let vault_id = get_vault_id::("OldVault"); mint_collateral::(&vault_id.account_id, (1u32 << 31).into()); let amount = wrapped(5); + Security::::set_active_block_number(1u32.into()); + + initialize_oracle::(); let threshold = UnsignedFixedPoint::::one(); VaultRegistry::::_set_secure_collateral_threshold(get_currency_pair::(), threshold); VaultRegistry::::_set_system_collateral_ceiling(get_currency_pair::(), 1_000_000_000u32.into()); @@ -151,8 +156,15 @@ benchmarks! { VaultRegistry::::issue_tokens(&vault_id, &amount).unwrap(); VaultRegistry::::try_increase_to_be_replaced_tokens(&vault_id, &amount).unwrap(); - // TODO: check that an amount was actually withdrawn + let vault : DefaultVault:: = VaultRegistry::::get_vault_from_id(&vault_id).expect("should return a vault"); + let to_be_replaced_tokens = vault.to_be_replaced_tokens; }: _(RawOrigin::Signed(vault_id.account_id.clone()), vault_id.currencies.clone(), amount.amount()) + verify { + let vault : DefaultVault:: = VaultRegistry::::get_vault_from_id(&vault_id).expect("should return a vault"); + let updated_to_be_replaced_tokens = vault.to_be_replaced_tokens; + + assert!(to_be_replaced_tokens > updated_to_be_replaced_tokens); + } accept_replace { initialize_oracle::(); diff --git a/testchain/node/src/cli.rs b/testchain/node/src/cli.rs index 58069092e..15ceaa062 100644 --- a/testchain/node/src/cli.rs +++ b/testchain/node/src/cli.rs @@ -10,7 +10,7 @@ pub struct Cli { } #[derive(Debug, clap::Subcommand)] -#[allow(clippy::large_enum_variant)] //todo: fix large size difference between variants +#[allow(clippy::large_enum_variant)] pub enum Subcommand { /// Key management cli utilities #[command(subcommand)] diff --git a/testchain/runtime/testnet/src/lib.rs b/testchain/runtime/testnet/src/lib.rs index 82a0d95b2..899fa6005 100644 --- a/testchain/runtime/testnet/src/lib.rs +++ b/testchain/runtime/testnet/src/lib.rs @@ -448,16 +448,13 @@ impl NativeCurrencyKey for SpacewalkNativeCurrencyKey { // because this is used in the benchmark_utils::DataCollector when feeding prices impl XCMCurrencyConversion for SpacewalkNativeCurrencyKey { fn convert_to_dia_currency_id(token_symbol: u8) -> Option<(Vec, Vec)> { - // todo: this code results in Execution error: - // todo: \"Unable to get required collateral for amount\": - // todo: Module(ModuleError { index: 19, error: [0, 0, 0, 0], message: None })", data: None - // } cfg_if::cfg_if! { - // if #[cfg(not(feature = "testing-utils"))] { - // if token_symbol == 0 { - // return Some((b"Kusama".to_vec(), b"KSM".to_vec())) - // } - // } - // } + cfg_if::cfg_if! { + if #[cfg(not(feature = "testing-utils"))] { + if token_symbol == 0 { + return Some((b"Kusama".to_vec(), b"KSM".to_vec())) + } + } + } // We assume that the blockchain is always 0 and the symbol represents the token symbol let blockchain = vec![0u8]; let symbol = vec![token_symbol];