diff --git a/Cargo.toml b/Cargo.toml index 25ee8dad..0182bdb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ [workspace] # List of crates included in this workspace members = [ - "core", + "arbiter-core", ] # Package configuration @@ -21,7 +21,7 @@ path = "bin/main.rs" # Dependency configuration [dependencies] # Local dependencies -core = { path = "core" } +arbiter-core = { path = "arbiter-core" } # External dependencies clap = { version = "4.3.0", features = ["derive"] } @@ -36,6 +36,8 @@ bytes = "1.4.0" # Simulation dependencies quote = "1.0.28" +statrs = "0.16.0" +RustQuant = "0.0.23" # Development dependencies [dev-dependencies] diff --git a/core/Cargo.toml b/arbiter-core/Cargo.toml similarity index 84% rename from core/Cargo.toml rename to arbiter-core/Cargo.toml index 9e0d18a6..08369594 100644 --- a/core/Cargo.toml +++ b/arbiter-core/Cargo.toml @@ -1,6 +1,6 @@ # Package configuration [package] -name = "core" +name = "arbiter-core" version = "0.3.0" edition = "2021" @@ -9,6 +9,7 @@ edition = "2021" # For execution bytes = "1.4.0" revm = "3.3.0" +statrs = "0.16.0" # For handling events crossbeam-channel = "0.5.8" @@ -17,6 +18,7 @@ crossbeam-channel = "0.5.8" rand = "0.8.5" rand_chacha = "0.3.1" rand_distr = "0.4.3" +RustQuant = "*" # For void middleware async-trait = "0.1.68" @@ -30,7 +32,7 @@ thiserror = "1.0.30" # url = "2.2.2" # Workspace dependencies -ethers = { git = "https://github.com/Autoparallel/ethers-rs.git"} +ethers = { git = "https://github.com/primitivefinance/ethers-rs.git"} # ethers-middleware = { version = "2.0.4" } serde = { version = "1.0.163", features= ["derive"]} serde_json = { version = "1.0.96" } diff --git a/core/LICENSE b/arbiter-core/LICENSE similarity index 100% rename from core/LICENSE rename to arbiter-core/LICENSE diff --git a/core/README.md b/arbiter-core/README.md similarity index 100% rename from core/README.md rename to arbiter-core/README.md diff --git a/core/src/agent.rs b/arbiter-core/src/agent.rs similarity index 91% rename from core/src/agent.rs rename to arbiter-core/src/agent.rs index 289b99d5..bf05924a 100644 --- a/core/src/agent.rs +++ b/arbiter-core/src/agent.rs @@ -28,7 +28,7 @@ pub struct Agent { } impl Agent { - pub(crate) fn new>(name: S) -> Self { + pub fn new>(name: S) -> Self { Self { name: name.into(), client: (), @@ -53,12 +53,8 @@ impl Agent { pub fn attach_to_environment(self, environment: &mut crate::environment::Environment) { let middleware = RevmMiddleware::new(&self, environment); - let agent = Agent::> { - name: self.name, - client: Arc::new(middleware), - behaviors: self.behaviors, - }; - environment.add_agent(agent); + let agent_attached = self.attach_to_client(middleware.into()); + environment.agents.push(agent_attached); } } @@ -75,9 +71,10 @@ pub(crate) mod tests { pub(crate) const TEST_AGENT_NAME: &str = "test_agent"; pub(crate) const TEST_BEHAVIOR_DATA: &str = "test_behavior_data"; - use super::*; use ethers::providers::{MockProvider, ProviderError}; + use super::*; + #[derive(Debug)] pub(crate) struct TestMiddleware {} diff --git a/core/src/bindings/arbiter_math.rs b/arbiter-core/src/bindings/arbiter_math.rs similarity index 100% rename from core/src/bindings/arbiter_math.rs rename to arbiter-core/src/bindings/arbiter_math.rs diff --git a/core/src/bindings/arbiter_token.rs b/arbiter-core/src/bindings/arbiter_token.rs similarity index 100% rename from core/src/bindings/arbiter_token.rs rename to arbiter-core/src/bindings/arbiter_token.rs diff --git a/core/src/bindings/liquid_exchange.rs b/arbiter-core/src/bindings/liquid_exchange.rs similarity index 100% rename from core/src/bindings/liquid_exchange.rs rename to arbiter-core/src/bindings/liquid_exchange.rs diff --git a/core/src/bindings/mod.rs b/arbiter-core/src/bindings/mod.rs similarity index 100% rename from core/src/bindings/mod.rs rename to arbiter-core/src/bindings/mod.rs diff --git a/core/src/environment.rs b/arbiter-core/src/environment.rs similarity index 69% rename from core/src/environment.rs rename to arbiter-core/src/environment.rs index 27456b70..cc3651ee 100644 --- a/core/src/environment.rs +++ b/arbiter-core/src/environment.rs @@ -2,6 +2,7 @@ #![warn(unsafe_code)] use crossbeam_channel::{unbounded, Receiver, Sender}; +use ethers::core::types::U64; use ethers::{providers::{JsonRpcClient, ProviderError}, types::{Filter, H256}, prelude::k256::sha2::{Digest, Sha256}, utils::serialize}; use revm::{ db::{CacheDB, EmptyDB}, @@ -15,15 +16,27 @@ use std::{ thread, collections::HashMap, }; -use crate::{utils::revm_logs_to_ethers_logs, agent::IsAttached}; -use crate::{agent::Agent, middleware::RevmMiddleware}; +use crate::{ + agent::{Agent, IsAttached, NotAttached}, + math::stochastic_process::{sample_poisson, SeededPoisson}, + middleware::RevmMiddleware, + utils::{convert_uint_to_u64, revm_logs_to_ethers_logs}, +}; /// Type Aliases for the event channel. pub(crate) type ToTransact = bool; -pub(crate) type ExecutionSender = Sender; +pub(crate) type ExecutionSender = Sender; pub(crate) type TxEnvSender = Sender<(ToTransact, TxEnv, ExecutionSender)>; pub(crate) type TxEnvReceiver = Receiver<(ToTransact, TxEnv, ExecutionSender)>; +/// Result struct for the [`Environment`]. that wraps the [`ExecutionResult`] and the block number. +#[derive(Debug, Clone)] +pub struct RevmResult { + pub(crate) result: ExecutionResult, + pub(crate) block_number: U64, +} + +/// State enum for the [`Environment`]. #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum State { /// The [`Environment`] is currently running. @@ -34,23 +47,27 @@ pub enum State { Stopped, } +/// The environment struct. pub struct Environment { + /// label for the environment pub label: String, pub(crate) state: State, pub(crate) evm: EVM>, pub(crate) tx_sender: TxEnvSender, - tx_receiver: TxEnvReceiver, + pub tx_receiver: TxEnvReceiver, pub(crate) event_broadcaster: Arc>, /// Clients (Agents) in the environment pub agents: Vec>>, - // pub deployed_contracts: HashMap>, + /// expected events per block + pub seeded_poisson: SeededPoisson, } // TODO: If the provider holds the connection then this can work better. +#[derive(Clone)] pub struct RevmProvider { pub(crate) tx_sender: TxEnvSender, - pub(crate) result_sender: crossbeam_channel::Sender, - pub(crate) result_receiver: crossbeam_channel::Receiver, + pub(crate) result_sender: crossbeam_channel::Sender, + pub(crate) result_receiver: crossbeam_channel::Receiver, pub(crate) event_receiver: crossbeam_channel::Receiver>, // pub(crate) filter_receivers: HashMap>>, // TODO: Use this to replace event_receivers so we can look for updates in specific filters } @@ -99,13 +116,16 @@ impl JsonRpcClient for RevmProvider { } impl Environment { - pub(crate) fn new>(label: S) -> Self { + /// Creates a new [`Environment`] with the given label. + pub(crate) fn new>(label: S, block_rate: f64, seed: u64) -> Self { let mut evm = EVM::new(); let db = CacheDB::new(EmptyDB {}); evm.database(db); + + let seeded_poisson = SeededPoisson::new(block_rate, seed); evm.env.cfg.limit_contract_code_size = Some(0x100000); // This is a large contract size limit, beware! evm.env.block.gas_limit = U256::MAX; - let (tx_sender, tx_receiver) = unbounded::<(ToTransact, TxEnv, Sender)>(); + let (tx_sender, tx_receiver) = unbounded::<(ToTransact, TxEnv, Sender)>(); Self { label: label.into(), state: State::Stopped, @@ -114,13 +134,17 @@ impl Environment { tx_receiver, event_broadcaster: Arc::new(Mutex::new(EventBroadcaster::new())), agents: vec![], + seeded_poisson, } } + + /// Creates a new [`Agent>) { - self.agents.push(agent); + // Waylon: I like them being created without a connection and then added to the environment where they will gain a connection. + pub fn add_agent(&mut self, agent: Agent) { + agent.attach_to_environment(self); } // TODO: Run should now run the agents as well as the evm. @@ -128,12 +152,26 @@ impl Environment { let tx_receiver = self.tx_receiver.clone(); let mut evm = self.evm.clone(); let event_broadcaster = self.event_broadcaster.clone(); + + let mut seeded_poisson = self.seeded_poisson.clone(); + + let mut counter: usize = 0; self.state = State::Running; - //give all agents their own thread and let them start watching for their evnts thread::spawn(move || { + let mut expected_events_per_block = seeded_poisson.sample(); + while let Ok((to_transact, tx, sender)) = tx_receiver.recv() { // Execute the transaction, echo the logs to all agents, and report the execution result to the agent who made the transaction. + if counter == expected_events_per_block { + counter = 0; + println!("EVM expected number of transactions reached. Moving to next block."); + println!("old block number: {:?}", evm.env.block.number); + evm.env.block.number += U256::from(1); + println!("new block number: {:?}", evm.env.block.number); + expected_events_per_block = seeded_poisson.sample(); + } + evm.env.tx = tx; if to_transact { let execution_result = match evm.transact_commit() { @@ -141,18 +179,28 @@ impl Environment { // URGENT: change this to a custom error Err(_) => panic!("failed"), }; + event_broadcaster .lock() .unwrap() .broadcast(execution_result.logs()); - sender.send(execution_result).unwrap(); + let revm_result = RevmResult { + result: execution_result, + block_number: convert_uint_to_u64(evm.env.block.number).unwrap(), + }; + sender.send(revm_result).unwrap(); + counter += 1; } else { let execution_result = match evm.transact() { Ok(val) => val, // URGENT: change this to a custom error Err(_) => panic!("failed"), }; - sender.send(execution_result.result).unwrap(); + let result_and_block = RevmResult { + result: execution_result.result, + block_number: convert_uint_to_u64(evm.env.block.number).unwrap(), + }; + sender.send(result_and_block).unwrap(); } } }); @@ -184,20 +232,27 @@ impl EventBroadcaster { #[cfg(test)] pub(crate) mod tests { + use std::time::Duration; + + use anyhow::{Ok, Result}; + use ethers::types::Address; + + use crate::bindings::arbiter_token::ArbiterToken; + use super::*; pub(crate) const TEST_ENV_LABEL: &str = "test"; #[test] fn new() { - let env = Environment::new(TEST_ENV_LABEL.to_string()); + let env = Environment::new(TEST_ENV_LABEL.to_string(), 1.0, 1); assert_eq!(env.label, TEST_ENV_LABEL); assert_eq!(env.state, State::Stopped); } #[test] fn run() { - let mut environment = Environment::new(TEST_ENV_LABEL.to_string()); + let mut environment = Environment::new(TEST_ENV_LABEL.to_string(), 1.0, 1); environment.run(); assert_eq!(environment.state, State::Running); } diff --git a/core/src/lib.rs b/arbiter-core/src/lib.rs similarity index 83% rename from core/src/lib.rs rename to arbiter-core/src/lib.rs index fa3827b2..49d89ed3 100644 --- a/core/src/lib.rs +++ b/arbiter-core/src/lib.rs @@ -8,6 +8,6 @@ pub mod environment; pub mod manager; pub mod math; pub mod middleware; -// #[cfg(test)] //TODO: UNCOMMENT THIS LATER +#[cfg(test)] //TODO: UNCOMMENT THIS LATER pub mod tests; pub mod utils; diff --git a/core/src/manager.rs b/arbiter-core/src/manager.rs similarity index 69% rename from core/src/manager.rs rename to arbiter-core/src/manager.rs index 75182979..ed0012d9 100644 --- a/core/src/manager.rs +++ b/arbiter-core/src/manager.rs @@ -5,10 +5,15 @@ use std::collections::HashMap; -use crate::environment::{Environment, State}; use anyhow::{anyhow, Result}; +use crate::{ + agent::{Agent, NotAttached}, + environment::{Environment, State}, +}; + /// Manages simulations. +#[derive(Default)] pub struct SimulationManager { /// The list of [`SimulationEnvironment`] that the simulation manager controls. pub environments: HashMap, @@ -23,17 +28,41 @@ impl SimulationManager { } /// Adds an environment to the [`SimulationManager`]'s list. - pub fn add_environment(&mut self, environment_label: String) -> Result<()> { + pub fn add_environment( + &mut self, + environment_label: String, + block_rate: f64, + seed: u64, + ) -> Result<()> { if self.environments.get(&environment_label).is_some() { return Err(anyhow!("Environment already exists.")); } self.environments.insert( environment_label.clone(), - Environment::new(environment_label), + Environment::new(environment_label, block_rate, seed), ); Ok(()) } + pub fn _stop_environemt(self, _environment_label: String) -> Result<()> { + todo!() + } + + /// adds an agent to an environment + pub fn add_agent( + &mut self, + agent: Agent, + environment_label: String, + ) -> Result<()> { + match self.environments.get_mut(&environment_label) { + Some(environment) => { + environment.add_agent(agent); + Ok(()) + } + None => Err(anyhow!("Environment does not exist.")), + } + } + /// Runs an environment that is in the [`SimulationManager`]'s list. pub fn run_environment(&mut self, environment_label: String) -> Result<()> { match self.environments.get_mut(&environment_label) { @@ -64,7 +93,7 @@ pub(crate) mod tests { fn add_environment() { let mut manager = SimulationManager::new(); let label = "test".to_string(); - manager.add_environment(label.clone()).unwrap(); + manager.add_environment(label.clone(), 1.0, 1).unwrap(); assert!(manager.environments.contains_key(&label)); } @@ -72,7 +101,7 @@ pub(crate) mod tests { fn run_environment() { let mut manager = SimulationManager::new(); let label = "test".to_string(); - manager.add_environment(label.clone()).unwrap(); + manager.add_environment(label.clone(), 1.0, 1).unwrap(); manager.run_environment(label.clone()).unwrap(); assert_eq!( manager.environments.get(&label).unwrap().state, diff --git a/core/src/math/mod.rs b/arbiter-core/src/math/mod.rs similarity index 99% rename from core/src/math/mod.rs rename to arbiter-core/src/math/mod.rs index 30106b3f..a2ce6a25 100644 --- a/core/src/math/mod.rs +++ b/arbiter-core/src/math/mod.rs @@ -1,7 +1,6 @@ //! Math module. use ethers::types::U256; - pub mod stochastic_process; /// Converts a float to a WAD fixed point prepared U256 number. diff --git a/arbiter-core/src/math/stochastic_process.rs b/arbiter-core/src/math/stochastic_process.rs new file mode 100644 index 00000000..e8f1e246 --- /dev/null +++ b/arbiter-core/src/math/stochastic_process.rs @@ -0,0 +1,144 @@ +use anyhow::{Ok, Result}; +use rand::{rngs::StdRng, SeedableRng}; +use rand_distr::Distribution as statrs_distribution; +use statrs::distribution::Poisson as statrs_poisson; + +pub use RustQuant::{ + statistics::distributions::{Distribution, Poisson}, + stochastics::{ + cox_ingersoll_ross::CoxIngersollRoss, extended_vasicek::ExtendedVasicek, ho_lee::HoLee, + hull_white::HullWhite, BlackDermanToy, BrownianMotion, OrnsteinUhlenbeck, + StochasticProcess, Trajectories, + }, +}; + +/// Sample Poisson process. +pub fn sample_poisson(lambda: f64) -> Result> { + let poisson = Poisson::new(lambda); + let float_samples = poisson.sample(1); + let int_sample: Vec = float_samples.iter().map(|&x| x.round() as i32).collect(); + Ok(int_sample) +} + +/// Poisson process with seed. +#[derive(Debug, Clone)] +pub struct SeededPoisson { + /// Poisson distribution. + pub distribution: statrs_poisson, + /// Random number generator. + pub rng: StdRng, +} + +/// Sample Poisson process with seed. +impl SeededPoisson { + /// Create new Poisson process with seed. + pub fn new(lambda: f64, seed: u64) -> Self { + let distribution = statrs_poisson::new(lambda).unwrap(); + let rng = StdRng::seed_from_u64(seed); + Self { distribution, rng } + } + /// Sample Poisson process. + pub fn sample(&mut self) -> usize { + self.distribution.sample(&mut self.rng) as usize + } +} + +#[cfg(test)] +mod tests { + + use RustQuant::stochastics::Sigma; + + use super::*; + + #[test] + fn seed_test() { + let mut test_dist_1 = SeededPoisson::new(10.0, 321); + let mut test_dist_2 = SeededPoisson::new(10000.0, 123); + let mut test_dist_3 = SeededPoisson::new(10000.0, 123); + + let result_1 = test_dist_1.sample(); + let result_2 = test_dist_1.sample(); + let result_3 = test_dist_2.sample(); + let result_4 = test_dist_2.sample(); + let result_5 = test_dist_3.sample(); + let result_6 = test_dist_3.sample(); + + assert_eq!(result_1, 15); + assert_eq!(result_2, 12); + assert_eq!(result_3, 9914); + assert_eq!(result_4, 10143); + assert_eq!(result_5, result_3); + assert_eq!(result_6, result_4); + } + + #[test] + pub fn brownian_motion() { + let bm = BrownianMotion::new(); + let trajectory = bm.euler_maruyama(10.0, 0.0, 0.5, 1000, 1000, false); + assert_eq!(trajectory.times.len(), 1001); + } + + #[test] + pub fn ornstein_uhlenbeck() { + let ou = OrnsteinUhlenbeck::new(1.0, 1.0, 1.0); + let trajectory = ou.euler_maruyama(10.0, 0.0, 0.5, 1000, 1000, false); + assert_eq!(trajectory.times.len(), 1001); + } + + #[test] + pub fn black_derman_toy() { + fn theta_t(_t: f64) -> f64 { + 1.5 + } + + let sig = Sigma::Const(0.13); + let bdt = BlackDermanToy::new(sig, theta_t); + let trajectory = bdt.euler_maruyama(10.0, 0.0, 0.5, 1000, 1000, false); + assert_eq!(trajectory.times.len(), 1001); + } + + #[test] + pub fn cox_ingersoll_ross() { + let cir = CoxIngersollRoss::new(0.15, 0.45, 0.01); + let trajectory = cir.euler_maruyama(10.0, 0.0, 0.5, 1000, 1000, false); + assert_eq!(trajectory.times.len(), 1001); + } + + #[test] + pub fn extended_vasicek() { + fn theta_t(_t: f64) -> f64 { + 0.5 + } + fn alpha_t(_t: f64) -> f64 { + 2.0 + } + let sig = 2.0; + + let ev = ExtendedVasicek::new(alpha_t, sig, theta_t); + let trajectory = ev.euler_maruyama(10.0, 0.0, 0.5, 1000, 1000, false); + assert_eq!(trajectory.times.len(), 1001); + } + + #[test] + pub fn ho_lee() { + fn theta_t(_t: f64) -> f64 { + 2.0 + } + let hl = HoLee::new(1.6, theta_t); + let trajectory = hl.euler_maruyama(10.0, 0.0, 0.5, 1000, 1000, false); + assert_eq!(trajectory.times.len(), 1001); + } + + #[test] + pub fn hull_white() { + fn theta_t(_t: f64) -> f64 { + 0.5 + } + let alpha = 2.0; + let sig = 2.0; + + let hw = HullWhite::new(alpha, sig, theta_t); + let trajectory = hw.euler_maruyama(10.0, 0.0, 0.5, 1000, 1000, false); + assert_eq!(trajectory.times.len(), 1001); + } +} diff --git a/core/src/middleware.rs b/arbiter-core/src/middleware.rs similarity index 88% rename from core/src/middleware.rs rename to arbiter-core/src/middleware.rs index 620f7dbb..9c4d457c 100644 --- a/core/src/middleware.rs +++ b/arbiter-core/src/middleware.rs @@ -3,35 +3,38 @@ //! This module contains the middleware for the Revm simulation environment. //! Most of the middleware is essentially a placeholder, but it is necessary to have a middleware to work with bindings more efficiently. -use ethers::prelude::pending_transaction::PendingTxState; -use ethers::providers::{FilterKind, JsonRpcClient, JsonRpcError, PendingTransaction, Provider}; +use std::{fmt::Debug, time::Duration}; + use ethers::utils; use ethers::{ - prelude::k256::{ - ecdsa::SigningKey, - sha2::{Digest, Sha256}, + prelude::{ + k256::{ + ecdsa::SigningKey, + sha2::{Digest, Sha256}, + }, + pending_transaction::PendingTxState, + ProviderError, + }, + providers::{ + FilterKind, FilterWatcher, Middleware, PendingTransaction, Provider, }, - prelude::ProviderError, - providers::{FilterWatcher, Middleware, MockProvider}, signers::{Signer, Wallet}, types::{transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Filter, Log}, }; -use rand::rngs::StdRng; -use rand::SeedableRng; -use revm::primitives::{ - result, CreateScheme, ExecutionResult, Output, TransactTo, TxEnv, B160, U256, +use rand::{rngs::StdRng, SeedableRng}; +use revm::primitives::{CreateScheme, ExecutionResult, Output, TransactTo, TxEnv, B160, U256}; + +use crate::{ + utils::{recast_address, revm_logs_to_ethers_logs}, + environment::{Environment, RevmProvider}, + agent::{Agent, NotAttached}, }; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::fmt::Debug; -use std::time::Duration; -use crate::agent::{Agent, NotAttached}; -use crate::environment::{Environment, RevmProvider}; -use crate::utils::{recast_address, revm_logs_to_ethers_logs}; // TODO: Refactor the connection and channels slightly to be more intuitive. For instance, the middleware may not really need to own a connection, but input one to set up everything else? -#[derive(Debug)] +/// The Revm middleware struct. +/// This struct is modular with ther ethers.rs middleware, and is used to connect the Revm environment in memory rather than over the network. +#[derive(Debug, Clone)] pub struct RevmMiddleware { provider: Provider, wallet: Wallet, @@ -120,9 +123,11 @@ impl Middleware for RevmMiddleware { self.provider.as_ref().result_sender.clone(), )) .unwrap(); - let result = self.provider.as_ref().result_receiver.recv().unwrap(); - let (output, revm_logs) = match result.clone() { - ExecutionResult::Success { output, logs, .. } => (output, logs), + + let revm_result = self.provider.as_ref().result_receiver.recv().unwrap(); + + let (output, revm_logs, block) = match revm_result.result.clone() { + ExecutionResult::Success { output, logs, .. } => (output, logs, revm_result.block_number), ExecutionResult::Revert { output, .. } => panic!("Failed due to revert: {:?}", output), ExecutionResult::Halt { reason, .. } => panic!("Failed due to halt: {:?}", reason), }; @@ -139,12 +144,10 @@ impl Middleware for RevmMiddleware { PendingTransaction::new(ethers::types::H256::zero(), self.provider()); let logs = revm_logs_to_ethers_logs(revm_logs); - pending_tx.state = PendingTxState::RevmTransactOutput(logs); + pending_tx.state = PendingTxState::RevmTransactOutput(logs, block); return Ok(pending_tx); } } - - // TODO: RECEIPTS OF TRANSACTIONS SHOULD STORE THE BLOCKNUMBERS THEY OCCURED IN } /// Makes a call to revm that will not commit a state change to the DB. Can be used for a mock transaction @@ -184,8 +187,9 @@ impl Middleware for RevmMiddleware { self.provider.as_ref().result_sender.clone(), )) .unwrap(); - let result = self.provider.as_ref().result_receiver.recv().unwrap(); - let output = match result.clone() { + + let revm_result = self.provider.as_ref().result_receiver.recv().unwrap(); + let output = match revm_result.result.clone() { ExecutionResult::Success { output, .. } => output, ExecutionResult::Revert { output, .. } => panic!("Failed due to revert: {:?}", output), ExecutionResult::Halt { reason, .. } => panic!("Failed due to halt: {:?}", reason), diff --git a/core/src/tests.rs b/arbiter-core/src/tests.rs similarity index 64% rename from core/src/tests.rs rename to arbiter-core/src/tests.rs index 32146187..1a830571 100644 --- a/core/src/tests.rs +++ b/arbiter-core/src/tests.rs @@ -1,26 +1,29 @@ -#[allow(missing_docs)] -use std::str::FromStr; +#![allow(missing_docs)] use std::sync::Arc; +use std::{ + str::FromStr, + sync::atomic::{AtomicUsize, Ordering}, +}; -use anyhow::Result; +use anyhow::{Ok, Result}; use ethers::{ prelude::{FilterWatcher, Middleware, StreamExt}, - types::{Address, Filter}, + types::{Address, Filter, U64}, }; use crate::{ - agent::{tests::*, *}, + agent::{tests::TEST_AGENT_NAME, *}, bindings::arbiter_token::*, - environment::{tests::*, *}, - manager::{tests::*, *}, - middleware::{tests::*, *}, + environment::{tests::TEST_ENV_LABEL, *}, + math::*, + middleware::*, }; -const TEST_ARG_NAME: &str = "ArbiterToken"; -const TEST_ARG_SYMBOL: &str = "ARBT"; -const TEST_ARG_DECIMALS: u8 = 18; -const TEST_MINT_AMOUNT: u128 = 1; -const TEST_MINT_TO: &str = "0xf7e93cc543d97af6632c9b8864417379dba4bf15"; +pub const TEST_ARG_NAME: &str = "ArbiterToken"; +pub const TEST_ARG_SYMBOL: &str = "ARBT"; +pub const TEST_ARG_DECIMALS: u8 = 18; +pub const TEST_MINT_AMOUNT: u128 = 1; +pub const TEST_MINT_TO: &str = "0xf7e93cc543d97af6632c9b8864417379dba4bf15"; #[test] fn token_mint() -> Result<()> { @@ -35,16 +38,15 @@ fn arbiter_math() -> Result<()> { // TODO: Finish off a test like this. #[test] fn attach_agent() { - let environment = &mut Environment::new(TEST_ENV_LABEL); + let environment = &mut Environment::new(TEST_ENV_LABEL, 1.0, 1); let agent = Agent::new(TEST_AGENT_NAME); agent.attach_to_environment(environment); assert_eq!(environment.agents[0].name, TEST_AGENT_NAME); } - #[test] fn simulation_agent_wallet() { - let environment = &mut Environment::new(TEST_ENV_LABEL); + let environment = &mut Environment::new(TEST_ENV_LABEL, 1.0, 1); let agent = Agent::new(TEST_AGENT_NAME); agent.attach_to_environment(environment); assert_eq!( @@ -55,12 +57,10 @@ fn simulation_agent_wallet() { #[test] fn multiple_agent_addresses() { - let environment = &mut Environment::new(TEST_ENV_LABEL); - let agent = - Agent::new(TEST_AGENT_NAME); + let environment = &mut Environment::new(TEST_ENV_LABEL, 1.0, 1); + let agent = Agent::new(TEST_AGENT_NAME); agent.attach_to_environment(environment); - let agent2 = - Agent::new(format!("new_{}", TEST_AGENT_NAME)); + let agent2 = Agent::new(format!("new_{}", TEST_AGENT_NAME)); agent2.attach_to_environment(environment); assert_ne!( environment.agents[0].client.default_sender(), @@ -75,7 +75,7 @@ fn agent_name_collision() { } async fn deploy() -> Result> { - let environment = &mut Environment::new(TEST_ENV_LABEL); + let environment = &mut Environment::new(TEST_ENV_LABEL, 1.0, 1); let agent = Agent::new(TEST_AGENT_NAME); agent.attach_to_environment(environment); environment.run(); @@ -173,3 +173,46 @@ async fn filter_watcher() -> Result<()> { // TODO: Test that we can filter out approvals and NOT transfers (or something like this) } + +// This test has two parts +// 1 check that the expected number of transactions per block is the actual number of transactions per block. +// 2 check the block number is incremented after the expected number of transactions is reached. +#[tokio::test] +async fn transaction_loop() -> Result<()> { + let mut env = Environment::new(TEST_ENV_LABEL, 2.0, 1); + + let mut dist = env.seeded_poisson.clone(); + let expected_tx_per_block = dist.sample(); + + println!("expected_tx_per_block: {}", expected_tx_per_block); + + let agent = Agent::new(TEST_AGENT_NAME); + env.add_agent(agent); + let agent = &env.agents[0]; + // tx_0 is the transaction that creates the token contract + let arbiter_token = deploy().await?; + + for index in 1..expected_tx_per_block { + println!("index: {}", index); + let tx = arbiter_token + .mint(agent.client.default_sender().unwrap(), 1000u64.into()) + .send() + .await + .unwrap() + .await + .unwrap() + .unwrap(); + + // minus 1 from deploy tx + if index < expected_tx_per_block - 1 { + let block_number = tx.block_number.unwrap(); + println!("block_number: {}", block_number); + assert_eq!(block_number, U64::from(0)); + } else { + let block_number = tx.block_number.unwrap(); + println!("block_number: {}", block_number); + assert_eq!(block_number, U64::from(1)); + } + } + Ok(()) +} diff --git a/core/src/utils.rs b/arbiter-core/src/utils.rs similarity index 65% rename from core/src/utils.rs rename to arbiter-core/src/utils.rs index 4fa1a38e..6a1c8630 100644 --- a/core/src/utils.rs +++ b/arbiter-core/src/utils.rs @@ -6,8 +6,11 @@ use std::{ }; use bytes::Bytes; -use ethers::{prelude::Address, types::H256}; -use revm::primitives::{ExecutionResult, Output, B160, B256}; +use ethers::{ + prelude::Address, + types::{H256, U64}, +}; +use revm::primitives::{ExecutionResult, Output, B160, B256, U256}; #[derive(Debug)] // We should use anyhow / thisError instead @@ -31,6 +34,11 @@ impl Display for UnpackError { } } +/// Recast a logs from Revm into the ethers.rs Log type. +/// # Arguments +/// * `revm_logs` - Logs from Revm. (Vec) +/// # Returns +/// * `Vec` - Logs recasted into ethers.rs Log type. pub fn revm_logs_to_ethers_logs( revm_logs: Vec, ) -> Vec { @@ -66,6 +74,11 @@ pub fn recast_address(address: B160) -> Address { Address::from(temp) } +/// Recast a B256 into an H256 type +/// # Arguments +/// * `input` - B256 to recast. (B256) +/// # Returns +/// * `H256` - Recasted H256. pub fn recast_b256(input: B256) -> H256 { let temp: [u8; 32] = input.as_bytes().try_into().unwrap(); H256::from(temp) @@ -98,3 +111,37 @@ pub fn unpack_execution(execution_result: ExecutionResult) -> Result Result { + let as_str = input.to_string(); + match as_str.parse::() { + Ok(val) => Ok(val.into()), + Err(_) => Err("U256 value is too large to fit into u64"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_conversion() { + // Test with a value that fits in u64. + let input = U256::from(10000); + assert_eq!(convert_uint_to_u64(input).unwrap(), U64::from(10000)); + + // Test with a value that is exactly at the limit of u64. + let input = U256::from(u64::MAX); + assert_eq!(convert_uint_to_u64(input).unwrap(), U64::from(u64::MAX)); + + // Test with a value that exceeds the limit of u64. + let input = U256::from(u64::MAX) + U256::from(1); + assert!(convert_uint_to_u64(input).is_err()); + } +} diff --git a/bin/init.rs b/bin/init.rs index 23ef0997..17eeff38 100644 --- a/bin/init.rs +++ b/bin/init.rs @@ -1,7 +1,6 @@ +use std::{fs, io::Write, path::Path}; + use quote::quote; -use std::fs; -use std::io::Write; -use std::path::Path; pub(crate) fn create_simulation(simulation_name: &str) -> std::io::Result<()> { let main = quote! { diff --git a/bin/main.rs b/bin/main.rs index a8f8b075..8a827fa2 100644 --- a/bin/main.rs +++ b/bin/main.rs @@ -4,7 +4,7 @@ use std::error::Error; -use clap::{arg, command, CommandFactory, Parser, Subcommand}; +use clap::{command, CommandFactory, Parser, Subcommand}; use eyre::Result; use thiserror::Error; diff --git a/core/src/math/stochastic_process.rs b/core/src/math/stochastic_process.rs deleted file mode 100644 index ef16f0ac..00000000 --- a/core/src/math/stochastic_process.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Module for price process generation and plotting. -use anyhow::Result; -use rand::SeedableRng; -use rand_distr::{Distribution, Normal}; -use serde::{Deserialize, Serialize}; - -// TODO: I think that this could probably be made a trait so that people can readily implement their own processes too. -// Errors also need to be better handled here. - -/// Enum for type of price process being used. -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(tag = "price_process_type", content = "price_process")] -pub enum PriceProcessType { - /// Geometric Brownian Motion (GBM) process. - GBM(GBM), - /// Ornstein-Uhlenbeck (OU) process. - OU(OU), -} - -/// Struct for all price processes init parameters. -/// A price process is a stochastic process that describes the evolution of a price_process. -/// # Fields -/// * `process_type` - Type of price process. (PriceProcessType) -/// * `timestep` - Time step of the simulation. (f64) -/// * `timescale` - Time in string interpretation. (String) -/// * `num_steps` - Number of steps in the simulation. (usize) -/// * `initial_price` - Initial price of the simulation. (f64) -/// * `seed` - Seed for testing. (u64) -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct PriceProcess { - /// Type of price process. - pub process_type: PriceProcessType, - /// Time step of the simulation. - pub timestep: f64, - /// Timescale in string interpretation. - pub timescale: String, - /// Number of steps in the simulation. - pub num_steps: usize, - /// Initial price of the simulation. - pub initial_price: f64, - /// Seed for testing. - pub seed: u64, -} - -impl PriceProcess { - /// Public builder function that instantiates a [`Price`]. - pub fn new( - process_type: PriceProcessType, - timestep: f64, - timescale: String, - num_steps: usize, - initial_price: f64, - seed: u64, - ) -> Self { - PriceProcess { - process_type, - timestep, - timescale, - num_steps, - initial_price, - seed, - } - } - - /// Generates a price path. - pub fn generate_price_path(&self) -> Result<(Vec, Vec)> { - match &self.process_type { - PriceProcessType::GBM(gbm) => gbm.generate(self), - PriceProcessType::OU(ou) => ou.generate(self), - } - } -} - -/// Geometric Brownian Motion process parameters struct. -/// # Fields -/// * `drift` - Price drift of the underlying asset. (f64) -/// * `volatility` - Volatility of the underlying asset. (f64) -#[derive(Copy, Clone, Serialize, Deserialize, Debug)] -pub struct GBM { - /// Price drift of the underlying asset. - pub drift: f64, - /// Volatility of the underlying asset. - pub volatility: f64, -} - -impl GBM { - /// Public builder function that instantiates a [`GBM`]. - pub fn new(drift: f64, volatility: f64) -> Self { - GBM { drift, volatility } - } - /// Generates a GBM price path. - /// # Arguments - /// * `timestep` - Time step of the simulation. (f64) - /// * `num_steps` - Number of steps in the simulation. (usize) - /// * `initial_price` - Initial price of the simulation. (f64) - /// * `seed` - Seed for testing. (u64) - /// # Returns - /// * `time` - Vector of time steps. (Vec) - /// * `prices` - Vector of prices. (Vec) - fn generate(&self, price_process: &PriceProcess) -> Result<(Vec, Vec)> { - let mut rng = rand::rngs::StdRng::seed_from_u64(price_process.seed); - let normal = Normal::new(0.0, 1.0)?; - let mut prices = vec![price_process.initial_price]; - let mut new_price = price_process.initial_price; - - for _ in 0..price_process.num_steps { - let noise = normal.sample(&mut rng); - new_price *= 1.0 - + self.drift * price_process.timestep - + self.volatility * noise * price_process.timestep.sqrt(); - prices.push(new_price); - } - let time = (0..price_process.num_steps) - .map(|i| i as f64 * price_process.timestep) - .collect::>(); - Ok((time, prices)) - } -} - -/// Ornstein-Uhlenbeck process parameters struct. -/// # Fields -/// * `volatility` - Volatility of the underlying asset. (f64) -/// * `mean_reversion_speed` - Mean reversion speed of the underlying asset. (f64) -/// * `mean_price` - Mean price of the underlying asset. (f64) -#[derive(Copy, Clone, Serialize, Deserialize, Debug)] -pub struct OU { - /// Volatility of the underlying asset. - pub volatility: f64, - /// Mean reversion speed of the underlying asset. - pub mean_reversion_speed: f64, - /// Mean price of the underlying asset. - pub mean_price: f64, -} - -impl OU { - /// Public builder function that instantiates a [`OU`]. - pub fn new(volatility: f64, mean_reversion_speed: f64, mean_price: f64) -> Self { - OU { - volatility, - mean_reversion_speed, - mean_price, - } - } - /// Generates an OU price path. - /// # Arguments - /// * `timestep` - Time step of the simulation. (f64) - /// * `num_steps` - Number of steps in the simulation. (usize) - /// * `initial_price` - Initial price of the simulation. (f64) - /// * `seed` - Seed for testing. (u64) - /// # Returns - /// * `time` - Vector of time steps. (Vec) - /// * `prices` - Vector of prices. (Vec) - fn generate(&self, price_process: &PriceProcess) -> Result<(Vec, Vec)> { - let mut rng = rand::rngs::StdRng::seed_from_u64(price_process.seed); - let normal = Normal::new(0.0, 1.0)?; - let mut prices = vec![price_process.initial_price]; - let mut new_price = price_process.initial_price; - - for _ in 0..price_process.num_steps { - let noise = normal.sample(&mut rng); - new_price += - self.mean_reversion_speed * (self.mean_price - new_price) * price_process.timestep - + self.volatility * noise * price_process.timestep.sqrt(); - prices.push(new_price); - } - let time = (0..price_process.num_steps) - .map(|i| i as f64 * price_process.timestep) - .collect::>(); - Ok((time, prices)) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn seeded_randomness_test() -> Result<()> { - let gbm = GBM::new(0.05, 0.3); - let price_process = PriceProcess::new( - PriceProcessType::GBM(gbm), - 0.1, - "Days".to_string(), - 100, - 100.0, - 1, - ); - // Test to see if same seed yields same result - let (time, prices) = price_process.generate_price_path()?; - let (time2, prices2) = price_process.generate_price_path()?; - assert_eq!(time, time2); - assert_eq!(prices, prices2); - // Test to see if different seed yields different result - let price_process_diff_seed = PriceProcess::new( - PriceProcessType::GBM(GBM::new(0.05, 0.3)), - 0.1, - "Days".to_string(), - 100, - 100.0, - 2, - ); - let (time3, prices3) = price_process_diff_seed.generate_price_path()?; - assert_eq!(time, time3); - assert_ne!(prices, prices3); - Ok(()) - } - - #[test] - fn gbm_step_test() -> Result<()> { - let gbm = GBM::new(0.05, 0.2); - let price_process = PriceProcess::new( - PriceProcessType::GBM(gbm), - 0.01, - "1D".to_string(), - 1, - 100.0, - 42, - ); - let (_, prices) = price_process.generate_price_path()?; - let initial_price = prices[0]; - let final_price = prices[1]; - - let mut rng = rand::rngs::StdRng::seed_from_u64(price_process.seed); - let expected_final_price = initial_price - // Check if the GBM is evolving as it should - * (1.0 + 0.05 * 0.01 + 0.2 * Normal::new(0.0, 1.0)?.sample(&mut rng) * (0.01_f64).sqrt()); - assert_eq!(final_price, expected_final_price); - Ok(()) - } -}