diff --git a/Cargo.lock b/Cargo.lock index 179c33fc5..b9f8e69c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4295,35 +4295,6 @@ dependencies = [ "tokio-stream", ] -[[package]] -name = "solana-bench-exchange" -version = "1.8.0" -dependencies = [ - "clap 2.33.3", - "itertools 0.10.1", - "log 0.4.14", - "num-derive", - "num-traits", - "rand 0.7.3", - "rayon", - "serde_json", - "serde_yaml", - "solana-clap-utils", - "solana-client", - "solana-core", - "solana-exchange-program", - "solana-faucet", - "solana-genesis", - "solana-gossip", - "solana-local-cluster", - "solana-logger 1.8.0", - "solana-metrics", - "solana-net-utils", - "solana-runtime", - "solana-sdk", - "solana-version", -] - [[package]] name = "solana-bench-streamer" version = "1.8.0" @@ -4685,23 +4656,6 @@ dependencies = [ "tar", ] -[[package]] -name = "solana-exchange-program" -version = "1.8.0" -dependencies = [ - "bincode", - "log 0.4.14", - "num-derive", - "num-traits", - "serde", - "serde_derive", - "solana-logger 1.8.0", - "solana-metrics", - "solana-runtime", - "solana-sdk", - "thiserror", -] - [[package]] name = "solana-failure-program" version = "1.8.0" @@ -4803,7 +4757,6 @@ dependencies = [ "serde_yaml", "solana-clap-utils", "solana-cli-config", - "solana-exchange-program", "solana-ledger", "solana-logger 1.8.0", "solana-runtime", @@ -5022,7 +4975,6 @@ dependencies = [ "solana-config-program", "solana-core", "solana-download-utils", - "solana-exchange-program", "solana-faucet", "solana-gossip", "solana-ledger", diff --git a/Cargo.toml b/Cargo.toml index f4d4e9bca..0bb226c4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ "accounts-cluster-bench", - "bench-exchange", "bench-streamer", "bench-tps", "accounts-bench", @@ -44,7 +43,6 @@ members = [ "program-test", "programs/bpf_loader", "programs/config", - "programs/exchange", "programs/failure", "programs/noop", "programs/ownable", diff --git a/bench-exchange/.gitignore b/bench-exchange/.gitignore deleted file mode 100644 index 252411f34..000000000 --- a/bench-exchange/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target/ -/config/ -/config-local/ -/farf/ diff --git a/bench-exchange/Cargo.toml b/bench-exchange/Cargo.toml deleted file mode 100644 index ec268b33d..000000000 --- a/bench-exchange/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -authors = ["Solana Maintainers "] -edition = "2018" -name = "solana-bench-exchange" -version = "1.8.0" -repository = "https://github.com/solana-labs/solana" -license = "Apache-2.0" -homepage = "https://solana.com/" -publish = false - -[dependencies] -clap = "2.33.1" -itertools = "0.10.1" -log = "0.4.14" -num-derive = "0.3" -num-traits = "0.2" -rand = "0.7.0" -rayon = "1.5.1" -serde_json = "1.0.64" -serde_yaml = "0.8.17" -solana-clap-utils = { path = "../clap-utils", version = "=1.8.0" } -solana-core = { path = "../core", version = "=1.8.0" } -solana-genesis = { path = "../genesis", version = "=1.8.0" } -solana-client = { path = "../client", version = "=1.8.0" } -solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" } -solana-faucet = { path = "../faucet", version = "=1.8.0" } -solana-gossip = { path = "../gossip", version = "=1.8.0" } -solana-logger = { path = "../logger", version = "=1.8.0" } -solana-metrics = { path = "../metrics", version = "=1.8.0" } -solana-net-utils = { path = "../net-utils", version = "=1.8.0" } -solana-runtime = { path = "../runtime", version = "=1.8.0" } -solana-sdk = { path = "../sdk", version = "=1.8.0" } -solana-version = { path = "../version", version = "=1.8.0" } - -[dev-dependencies] -solana-local-cluster = { path = "../local-cluster", version = "=1.8.0" } - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/bench-exchange/README.md b/bench-exchange/README.md deleted file mode 100644 index 1377680a9..000000000 --- a/bench-exchange/README.md +++ /dev/null @@ -1,479 +0,0 @@ -# token-exchange -Solana Token Exchange Bench - -If you can't wait; jump to [Running the exchange](#Running-the-exchange) to -learn how to start and interact with the exchange. - -### Table of Contents -[Overview](#Overview)
-[Premise](#Premise)
-[Exchange startup](#Exchange-startup)
-[Order Requests](#Trade-requests)
-[Order Cancellations](#Trade-cancellations)
-[Trade swap](#Trade-swap)
-[Exchange program operations](#Exchange-program-operations)
-[Quotes and OHLCV](#Quotes-and-OHLCV)
-[Investor strategies](#Investor-strategies)
-[Running the exchange](#Running-the-exchange)
- -## Overview - -An exchange is a marketplace where one asset can be traded for another. This -demo demonstrates one way to host an exchange on the Solana blockchain by -emulating a currency exchange. - -The assets are virtual tokens held by investors who may post order requests to -the exchange. A Matcher monitors the exchange and posts swap requests for -matching orders. All the transactions can execute concurrently. - -## Premise - -- Exchange - - An exchange is a marketplace where one asset can be traded for another. - The exchange in this demo is the on-chain program that implements the - tokens and the policies for trading those tokens. -- Token - - A virtual asset that can be owned, traded, and holds virtual intrinsic value - compared to other assets. There are four types of tokens in this demo, A, - B, C, D. Each one may be traded for another. -- Token account - - An account owned by the exchange that holds a quantity of one type of token. -- Account request - - A request to create a token account -- Token request - - A request to deposit tokens of a particular type into a token account. -- Asset pair - - A struct with fields Base and Quote, representing the two assets which make up a - trading pair, which themselves are Tokens. The Base or 'primary' asset is the - numerator and the Quote is the denominator for pricing purposes. -- Order side - - Describes which side of the market an investor wants to place a trade on. Options - are "Bid" or "Ask", where a bid represents an offer to purchase the Base asset of - the AssetPair for a sum of the Quote Asset and an Ask is an offer to sell Base asset - for the Quote asset. -- Price ratio - - An expression of the relative prices of two tokens. Calculated with the Base - Asset as the numerator and the Quote Asset as the denominator. Ratios are - represented as fixed point numbers. The fixed point scaler is defined in - [exchange_state.rs](https://github.com/solana-labs/solana/blob/c2fdd1362a029dcf89c8907c562d2079d977df11/programs/exchange_api/src/exchange_state.rs#L7) -- Order request - - A Solana transaction sent by a trader to the exchange to submit an order. - Order requests are made up of the token pair, the order side (bid or ask), - quantity of the primary token, the price ratio, and the two token accounts - to be credited/deducted. An example trade request looks like "T AB 5 2" - which reads "Exchange 5 A tokens to B tokens at a price ratio of 1:2" A fulfilled trade would result in 5 A tokens - deducted and 10 B tokens credited to the trade initiator's token accounts. - Successful order requests result in an order. -- Order - - The result of a successful order request. orders are stored in - accounts owned by the submitter of the order request. They can only be - canceled by their owner but can be used by anyone in a trade swap. They - contain the same information as the order request. -- Price spread - - The difference between the two matching orders. The spread is the - profit of the Matcher initiating the swap request. -- Match requirements - - Policies that result in a successful trade swap. -- Match request - - A request to fill two complementary orders (bid/ask), resulting if successful, - in a trade being created. -- Trade - - A successful trade is created from two matching orders that meet - swap requirements which are submitted in a Match Request by a Matcher and - executed by the exchange. A trade may not wholly satisfy one or both of the - orders in which case the orders are adjusted appropriately. Upon execution, - tokens are distributed to the traders' accounts and any overlap or - "negative spread" between orders is deposited into the Matcher's profit - account. All successful trades are recorded in the data of a new solana - account for posterity. -- Investor - - Individual investors who hold a number of tokens and wish to trade them on - the exchange. Investors operate as Solana thin clients who own a set of - accounts containing tokens and/or order requests. Investors post - transactions to the exchange in order to request tokens and post or cancel - order requests. -- Matcher - - An agent who facilitates trading between investors. Matchers operate as - Solana thin clients who monitor all the orders looking for a trade - match. Once found, the Matcher issues a swap request to the exchange. - Matchers are the engine of the exchange and are rewarded for their efforts by - accumulating the price spreads of the swaps they initiate. Matchers also - provide current bid/ask price and OHLCV (Open, High, Low, Close, Volume) - information on demand via a public network port. -- Transaction fees - - Solana transaction fees are paid for by the transaction submitters who are - the Investors and Matchers. - -## Exchange startup - -The exchange is up and running when it reaches a state where it can take -investors' trades and Matchers' match requests. To achieve this state the -following must occur in order: - -- Start the Solana blockchain -- Start the thin-client -- The Matcher subscribes to change notifications for all the accounts owned by - the exchange program id. The subscription is managed via Solana's JSON RPC - interface. -- The Matcher starts responding to queries for bid/ask price and OHLCV - -The Matcher responding successfully to price and OHLCV requests is the signal to -the investors that trades submitted after that point will be analyzed. - -Investors will initially query the exchange to discover their current balance -for each type of token. If the investor does not already have an account for -each type of token, they will submit account requests. Matcher as well will -request accounts to hold the tokens they earn by initiating trade swaps. - -```rust -/// Supported token types -pub enum Token { - A, - B, - C, - D, -} - -/// Supported token pairs -pub enum TokenPair { - AB, - AC, - AD, - BC, - BD, - CD, -} - -pub enum ExchangeInstruction { - /// New token account - /// key 0 - Signer - /// key 1 - New token account - AccountRequest, -} - -/// Token accounts are populated with this structure -pub struct TokenAccountInfo { - /// Investor who owns this account - pub owner: Pubkey, - /// Current number of tokens this account holds - pub tokens: Tokens, -} -``` - -For this demo investors or Matcher can request more tokens from the exchange at -any time by submitting token requests. In non-demos, an exchange of this type -would provide another way to exchange a 3rd party asset into tokens. - -To request tokens, investors submit transfer requests: - -```rust -pub enum ExchangeInstruction { - /// Transfer tokens between two accounts - /// key 0 - Account to transfer tokens to - /// key 1 - Account to transfer tokens from. This can be the exchange program itself, - /// the exchange has a limitless number of tokens it can transfer. - TransferRequest(Token, u64), -} -``` - -## Order Requests - -When an investor decides to exchange a token of one type for another, they -submit a transaction to the Solana Blockchain containing an order request, which, -if successful, is turned into an order. orders do not expire but are -cancellable. When an order is created, tokens are deducted from a token -account and the order acts as an escrow. The tokens are held until the -order is fulfilled or canceled. If the direction is `To`, then the number -of `tokens` are deducted from the primary account, if `From` then `tokens` -multiplied by `price` are deducted from the secondary account. orders are -no longer valid when the number of `tokens` goes to zero, at which point they -can no longer be used. - -```rust -/// Direction of the exchange between two tokens in a pair -pub enum Direction { - /// Trade first token type (primary) in the pair 'To' the second - To, - /// Trade first token type in the pair 'From' the second (secondary) - From, -} - -pub struct OrderRequestInfo { - /// Direction of trade - pub direction: Direction, - - /// Token pair to trade - pub pair: TokenPair, - - /// Number of tokens to exchange; refers to the primary or the secondary depending on the direction - pub tokens: u64, - - /// The price ratio the primary price over the secondary price. The primary price is fixed - /// and equal to the variable `SCALER`. - pub price: u64, - - /// Token account to deposit tokens on successful swap - pub dst_account: Pubkey, -} - -pub enum ExchangeInstruction { - /// order request - /// key 0 - Signer - /// key 1 - Account in which to record the swap - /// key 2 - Token account associated with this trade - TradeRequest(TradeRequestInfo), -} - -/// Trade accounts are populated with this structure -pub struct TradeOrderInfo { - /// Owner of the order - pub owner: Pubkey, - /// Direction of the exchange - pub direction: Direction, - /// Token pair indicating two tokens to exchange, first is primary - pub pair: TokenPair, - /// Number of tokens to exchange; primary or secondary depending on direction - pub tokens: u64, - /// Scaled price of the secondary token given the primary is equal to the scale value - /// If scale is 1 and price is 2 then ratio is 1:2 or 1 primary token for 2 secondary tokens - pub price: u64, - /// account which the tokens were source from. The trade account holds the tokens in escrow - /// until either one or more part of a swap or the trade is canceled. - pub src_account: Pubkey, - /// account which the tokens the tokens will be deposited into on a successful trade - pub dst_account: Pubkey, -} -``` - -## Order cancellations - -An investor may cancel a trade at anytime, but only trades they own. If the -cancellation is successful, any tokens held in escrow are returned to the -account from which they came. - -```rust -pub enum ExchangeInstruction { - /// order cancellation - /// key 0 - Signer - /// key 1 -order to cancel - TradeCancellation, -} -``` - -## Trade swaps - -The Matcher is monitoring the accounts assigned to the exchange program and -building a trade-order table. The order table is used to identify -matching orders which could be fulfilled. When a match is found the -Matcher should issue a swap request. Swap requests may not satisfy the entirety -of either order, but the exchange will greedily fulfill it. Any leftover tokens -in either account will keep the order valid for further swap requests in -the future. - -Matching orders are defined by the following swap requirements: - -- Opposite polarity (one `To` and one `From`) -- Operate on the same token pair -- The price ratio of the `From` order is greater than or equal to the `To` order -- There are sufficient tokens to perform the trade - -Orders can be written in the following format: - -`investor direction pair quantity price-ratio` - -For example: - -- `1 T AB 2 1` - - Investor 1 wishes to exchange 2 A tokens to B tokens at a ratio of 1 A to 1 - B -- `2 F AC 6 1.2` - - Investor 2 wishes to exchange A tokens from 6 B tokens at a ratio of 1 A - from 1.2 B - -An order table could look something like the following. Notice how the columns -are sorted low to high and high to low, respectively. Prices are dramatic and -whole for clarity. - -|Row| To | From | -|---|-------------|------------| -| 1 | 1 T AB 2 4 | 2 F AB 2 8 | -| 2 | 1 T AB 1 4 | 2 F AB 2 8 | -| 3 | 1 T AB 6 6 | 2 F AB 2 7 | -| 4 | 1 T AB 2 8 | 2 F AB 3 6 | -| 5 | 1 T AB 2 10 | 2 F AB 1 5 | - -As part of a successful swap request, the exchange will credit tokens to the -Matcher's account equal to the difference in the price ratios or the two orders. -These tokens are considered the Matcher's profit for initiating the trade. - -The Matcher would initiate the following swap on the order table above: - - - Row 1, To: Investor 1 trades 2 A tokens to 8 B tokens - - Row 1, From: Investor 2 trades 2 A tokens from 8 B tokens - - Matcher takes 8 B tokens as profit - -Both row 1 trades are fully realized, table becomes: - -|Row| To | From | -|---|-------------|------------| -| 1 | 1 T AB 1 4 | 2 F AB 2 8 | -| 2 | 1 T AB 6 6 | 2 F AB 2 7 | -| 3 | 1 T AB 2 8 | 2 F AB 3 6 | -| 4 | 1 T AB 2 10 | 2 F AB 1 5 | - -The Matcher would initiate the following swap: - - - Row 1, To: Investor 1 trades 1 A token to 4 B tokens - - Row 1, From: Investor 2 trades 1 A token from 4 B tokens - - Matcher takes 4 B tokens as profit - -Row 1 From is not fully realized, table becomes: - -|Row| To | From | -|---|-------------|------------| -| 1 | 1 T AB 6 6 | 2 F AB 1 8 | -| 2 | 1 T AB 2 8 | 2 F AB 2 7 | -| 3 | 1 T AB 2 10 | 2 F AB 3 6 | -| 4 | | 2 F AB 1 5 | - -The Matcher would initiate the following swap: - - - Row 1, To: Investor 1 trades 1 A token to 6 B tokens - - Row 1, From: Investor 2 trades 1 A token from 6 B tokens - - Matcher takes 2 B tokens as profit - -Row 1 To is now fully realized, table becomes: - -|Row| To | From | -|---|-------------|------------| -| 1 | 1 T AB 5 6 | 2 F AB 2 7 | -| 2 | 1 T AB 2 8 | 2 F AB 3 5 | -| 3 | 1 T AB 2 10 | 2 F AB 1 5 | - -The Matcher would initiate the following last swap: - - - Row 1, To: Investor 1 trades 2 A token to 12 B tokens - - Row 1, From: Investor 2 trades 2 A token from 12 B tokens - - Matcher takes 2 B tokens as profit - -Table becomes: - -|Row| To | From | -|---|-------------|------------| -| 1 | 1 T AB 3 6 | 2 F AB 3 5 | -| 2 | 1 T AB 2 8 | 2 F AB 1 5 | -| 3 | 1 T AB 2 10 | | - -At this point the lowest To's price is larger than the largest From's price so -no more swaps would be initiated until new orders came in. - -```rust -pub enum ExchangeInstruction { - /// Trade swap request - /// key 0 - Signer - /// key 1 - Account in which to record the swap - /// key 2 - 'To' order - /// key 3 - `From` order - /// key 4 - Token account associated with the To Trade - /// key 5 - Token account associated with From trade - /// key 6 - Token account in which to deposit the Matcher profit from the swap. - SwapRequest, -} - -/// Swap accounts are populated with this structure -pub struct TradeSwapInfo { - /// Pair swapped - pub pair: TokenPair, - /// `To` order - pub to_trade_order: Pubkey, - /// `From` order - pub from_trade_order: Pubkey, - /// Number of primary tokens exchanged - pub primary_tokens: u64, - /// Price the primary tokens were exchanged for - pub primary_price: u64, - /// Number of secondary tokens exchanged - pub secondary_tokens: u64, - /// Price the secondary tokens were exchanged for - pub secondary_price: u64, -} -``` - -## Exchange program operations - -Putting all the commands together from above, the following operations will be -supported by the on-chain exchange program: - -```rust -pub enum ExchangeInstruction { - /// New token account - /// key 0 - Signer - /// key 1 - New token account - AccountRequest, - - /// Transfer tokens between two accounts - /// key 0 - Account to transfer tokens to - /// key 1 - Account to transfer tokens from. This can be the exchange program itself, - /// the exchange has a limitless number of tokens it can transfer. - TransferRequest(Token, u64), - - /// order request - /// key 0 - Signer - /// key 1 - Account in which to record the swap - /// key 2 - Token account associated with this trade - TradeRequest(TradeRequestInfo), - - /// order cancellation - /// key 0 - Signer - /// key 1 -order to cancel - TradeCancellation, - - /// Trade swap request - /// key 0 - Signer - /// key 1 - Account in which to record the swap - /// key 2 - 'To' order - /// key 3 - `From` order - /// key 4 - Token account associated with the To Trade - /// key 5 - Token account associated with From trade - /// key 6 - Token account in which to deposit the Matcher profit from the swap. - SwapRequest, -} -``` - -## Quotes and OHLCV - -The Matcher will provide current bid/ask price quotes based on trade actively and -also provide OHLCV based on some time window. The details of how the bid/ask -price quotes are calculated are yet to be decided. - -## Investor strategies - -To make a compelling demo, the investors needs to provide interesting trade -behavior. Something as simple as a randomly twiddled baseline would be a -minimum starting point. - -## Running the exchange - -The exchange bench posts trades and swaps matches as fast as it can. - -You might want to bump the duration up -to 60 seconds and the batch size to 1000 for better numbers. You can modify those -in client_demo/src/demo.rs::test_exchange_local_cluster. - -The following command runs the bench: - -```bash -$ RUST_LOG=solana_bench_exchange=info cargo test --release -- --nocapture test_exchange_local_cluster -``` - -To also see the cluster messages: - -```bash -$ RUST_LOG=solana_bench_exchange=info,solana=info cargo test --release -- --nocapture test_exchange_local_cluster -``` diff --git a/bench-exchange/src/bench.rs b/bench-exchange/src/bench.rs deleted file mode 100644 index cffb7605b..000000000 --- a/bench-exchange/src/bench.rs +++ /dev/null @@ -1,1028 +0,0 @@ -#![allow(clippy::useless_attribute)] -#![allow(clippy::integer_arithmetic)] - -use crate::order_book::*; -use itertools::izip; -use log::*; -use rand::{thread_rng, Rng}; -use rayon::prelude::*; -use solana_client::perf_utils::{sample_txs, SampleStats}; -use solana_core::gen_keys::GenKeys; -use solana_exchange_program::{exchange_instruction, exchange_state::*, id}; -use solana_faucet::faucet::request_airdrop_transaction; -use solana_genesis::Base64Account; -use solana_metrics::datapoint_info; -use solana_sdk::{ - client::{Client, SyncClient}, - commitment_config::CommitmentConfig, - message::Message, - pubkey::Pubkey, - signature::{Keypair, Signer}, - timing::{duration_as_ms, duration_as_s}, - transaction::Transaction, - {system_instruction, system_program}, -}; -use std::{ - cmp, - collections::{HashMap, VecDeque}, - fs::File, - io::prelude::*, - mem, - net::SocketAddr, - path::Path, - process::exit, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - mpsc::{channel, Receiver, Sender}, - Arc, RwLock, - }, - thread::{sleep, Builder}, - time::{Duration, Instant}, -}; - -// TODO Chunk length as specified results in a bunch of failures, divide by 10 helps... -// Assume 4MB network buffers, and 512 byte packets -const FUND_CHUNK_LEN: usize = 4 * 1024 * 1024 / 512; - -// Maximum system transfers per transaction -const MAX_TRANSFERS_PER_TX: u64 = 4; - -pub type SharedTransactions = Arc>>>; - -pub struct Config { - pub identity: Keypair, - pub threads: usize, - pub duration: Duration, - pub transfer_delay: u64, - pub fund_amount: u64, - pub batch_size: usize, - pub chunk_size: usize, - pub account_groups: usize, - pub client_ids_and_stake_file: String, - pub read_from_client_file: bool, -} - -impl Default for Config { - fn default() -> Self { - Self { - identity: Keypair::new(), - threads: 4, - duration: Duration::new(u64::max_value(), 0), - transfer_delay: 0, - fund_amount: 100_000, - batch_size: 10, - chunk_size: 10, - account_groups: 100, - client_ids_and_stake_file: String::new(), - read_from_client_file: false, - } - } -} - -pub fn create_client_accounts_file( - client_ids_and_stake_file: &str, - batch_size: usize, - account_groups: usize, - fund_amount: u64, -) { - let accounts_in_groups = batch_size * account_groups; - const NUM_KEYPAIR_GROUPS: u64 = 2; - let total_keys = accounts_in_groups as u64 * NUM_KEYPAIR_GROUPS; - - let keypairs = generate_keypairs(total_keys); - - let mut accounts = HashMap::new(); - keypairs.iter().for_each(|keypair| { - accounts.insert( - serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(), - Base64Account { - balance: fund_amount, - executable: false, - owner: system_program::id().to_string(), - data: String::new(), - }, - ); - }); - - let serialized = serde_yaml::to_string(&accounts).unwrap(); - let path = Path::new(&client_ids_and_stake_file); - let mut file = File::create(path).unwrap(); - file.write_all(&serialized.into_bytes()).unwrap(); -} - -pub fn do_bench_exchange(clients: Vec, config: Config) -where - T: 'static + Client + Send + Sync, -{ - let Config { - identity, - threads, - duration, - transfer_delay, - fund_amount, - batch_size, - chunk_size, - account_groups, - client_ids_and_stake_file, - read_from_client_file, - } = config; - - info!( - "Exchange client: threads {} duration {} fund_amount {}", - threads, - duration_as_s(&duration), - fund_amount - ); - info!( - "Exchange client: transfer delay {} batch size {} chunk size {}", - transfer_delay, batch_size, chunk_size - ); - - let accounts_in_groups = batch_size * account_groups; - const NUM_KEYPAIR_GROUPS: u64 = 2; - let total_keys = accounts_in_groups as u64 * NUM_KEYPAIR_GROUPS; - - let mut signer_keypairs = if read_from_client_file { - let path = Path::new(&client_ids_and_stake_file); - let file = File::open(path).unwrap(); - - let accounts: HashMap = serde_yaml::from_reader(file).unwrap(); - accounts - .into_iter() - .map(|(keypair, _)| { - let bytes: Vec = serde_json::from_str(keypair.as_str()).unwrap(); - Keypair::from_bytes(&bytes).unwrap() - }) - .collect() - } else { - info!("Generating {:?} signer keys", total_keys); - generate_keypairs(total_keys) - }; - - let trader_signers: Vec<_> = signer_keypairs - .drain(0..accounts_in_groups) - .map(Arc::new) - .collect(); - let swapper_signers: Vec<_> = signer_keypairs - .drain(0..accounts_in_groups) - .map(Arc::new) - .collect(); - - let clients: Vec<_> = clients.into_iter().map(Arc::new).collect(); - let client = clients[0].as_ref(); - - if !read_from_client_file { - info!("Fund trader accounts"); - fund_keys(client, &identity, &trader_signers, fund_amount); - info!("Fund swapper accounts"); - fund_keys(client, &identity, &swapper_signers, fund_amount); - } - - info!("Generating {:?} account keys", total_keys); - let mut account_keypairs = generate_keypairs(total_keys); - let src_keypairs: Vec<_> = account_keypairs.drain(0..accounts_in_groups).collect(); - let src_pubkeys: Vec = src_keypairs - .iter() - .map(|keypair| keypair.pubkey()) - .collect(); - - let profit_keypairs: Vec<_> = account_keypairs.drain(0..accounts_in_groups).collect(); - let profit_pubkeys: Vec = profit_keypairs - .iter() - .map(|keypair| keypair.pubkey()) - .collect(); - - info!("Create {:?} source token accounts", src_pubkeys.len()); - create_token_accounts(client, &trader_signers, &src_keypairs); - info!("Create {:?} profit token accounts", profit_pubkeys.len()); - create_token_accounts(client, &swapper_signers, &profit_keypairs); - - // Collect the max transaction rate and total tx count seen (single node only) - let sample_stats = Arc::new(RwLock::new(Vec::new())); - let sample_period = 1; // in seconds - info!("Sampling clients for tps every {} s", sample_period); - info!( - "Requesting and swapping trades with {} ms delay per thread...", - transfer_delay - ); - - let exit_signal = Arc::new(AtomicBool::new(false)); - let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new())); - let total_txs_sent_count = Arc::new(AtomicUsize::new(0)); - let s_threads: Vec<_> = (0..threads) - .map(|_| { - let exit_signal = exit_signal.clone(); - let shared_txs = shared_txs.clone(); - let total_txs_sent_count = total_txs_sent_count.clone(); - let client = clients[0].clone(); - Builder::new() - .name("solana-exchange-transfer".to_string()) - .spawn(move || { - do_tx_transfers(&exit_signal, &shared_txs, &total_txs_sent_count, &client) - }) - .unwrap() - }) - .collect(); - - trace!("Start swapper thread"); - let (swapper_sender, swapper_receiver) = channel(); - let swapper_thread = { - let exit_signal = exit_signal.clone(); - let shared_txs = shared_txs.clone(); - let client = clients[0].clone(); - Builder::new() - .name("solana-exchange-swapper".to_string()) - .spawn(move || { - swapper( - &exit_signal, - &swapper_receiver, - &shared_txs, - &swapper_signers, - &profit_pubkeys, - transfer_delay, - batch_size, - chunk_size, - account_groups, - &client, - ) - }) - .unwrap() - }; - - trace!("Start trader thread"); - let trader_thread = { - let exit_signal = exit_signal.clone(); - - let client = clients[0].clone(); - Builder::new() - .name("solana-exchange-trader".to_string()) - .spawn(move || { - trader( - &exit_signal, - &swapper_sender, - &shared_txs, - &trader_signers, - &src_pubkeys, - transfer_delay, - batch_size, - chunk_size, - account_groups, - &client, - ) - }) - .unwrap() - }; - - let sample_threads: Vec<_> = clients - .iter() - .map(|client| { - let exit_signal = exit_signal.clone(); - let sample_stats = sample_stats.clone(); - let client = client.clone(); - Builder::new() - .name("solana-exchange-sample".to_string()) - .spawn(move || sample_txs(&exit_signal, &sample_stats, sample_period, &client)) - .unwrap() - }) - .collect(); - - sleep(duration); - - info!("Stopping threads"); - exit_signal.store(true, Ordering::Relaxed); - info!("Wait for trader thread"); - let _ = trader_thread.join(); - info!("Waiting for swapper thread"); - let _ = swapper_thread.join(); - info!("Wait for tx threads"); - for t in s_threads { - let _ = t.join(); - } - info!("Wait for sample threads"); - for t in sample_threads { - let _ = t.join(); - } - - compute_and_report_stats( - &sample_stats, - total_txs_sent_count.load(Ordering::Relaxed) as u64, - ); -} - -fn do_tx_transfers( - exit_signal: &Arc, - shared_txs: &SharedTransactions, - total_txs_sent_count: &Arc, - client: &Arc, -) where - T: Client, -{ - loop { - let txs; - { - let mut shared_txs_wl = shared_txs.write().unwrap(); - txs = shared_txs_wl.pop_front(); - } - if let Some(txs0) = txs { - let n = txs0.len(); - - let now = Instant::now(); - for tx in txs0 { - client.async_send_transaction(tx).expect("Transfer"); - } - let duration = now.elapsed(); - - total_txs_sent_count.fetch_add(n, Ordering::Relaxed); - datapoint_info!( - "bench-exchange-do_tx_transfers", - ("duration", duration_as_ms(&duration), i64), - ("count", n, i64) - ); - } - if exit_signal.load(Ordering::Relaxed) { - return; - } - } -} - -struct TradeInfo { - trade_account: Pubkey, - order_info: OrderInfo, -} -#[allow(clippy::too_many_arguments)] -fn swapper( - exit_signal: &Arc, - receiver: &Receiver>, - shared_txs: &SharedTransactions, - signers: &[Arc], - profit_pubkeys: &[Pubkey], - transfer_delay: u64, - batch_size: usize, - chunk_size: usize, - account_groups: usize, - client: &Arc, -) where - T: Client, -{ - let mut order_book = OrderBook::default(); - let mut account_group: usize = 0; - - let mut txs = 0; - let mut total_txs = 0; - let mut now = Instant::now(); - let start_time = now; - let mut total_elapsed = start_time.elapsed(); - - // Chunks may have been dropped and we don't want to wait a long time - // for each time, Back-off each time we fail to confirm a chunk - const CHECK_TX_TIMEOUT_MAX_MS: u64 = 15000; - const CHECK_TX_DELAY_MS: u64 = 100; - let mut max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS; - - // If we dump too many chunks maybe we are just waiting on a back-log - // rather than a series of dropped packets, reset to max waits - const MAX_DUMPS: u64 = 50; - let mut dumps = 0; - - 'outer: loop { - if let Ok(trade_infos) = receiver.try_recv() { - let mut tries = 0; - let mut trade_index = 0; - while client - .get_balance_with_commitment( - &trade_infos[trade_index].trade_account, - CommitmentConfig::processed(), - ) - .unwrap_or(0) - == 0 - { - tries += 1; - if tries >= max_tries { - if exit_signal.load(Ordering::Relaxed) { - break 'outer; - } - error!("Give up and dump batch"); - if dumps >= MAX_DUMPS { - error!("Max batches dumped, reset wait back-off"); - max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS; - dumps = 0; - } else { - dumps += 1; - max_tries /= 2; - } - continue 'outer; - } - debug!("{} waiting for trades batch to clear", tries); - sleep(Duration::from_millis(CHECK_TX_DELAY_MS)); - trade_index = thread_rng().gen_range(0, trade_infos.len()); - } - max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS; - dumps = 0; - - trade_infos.iter().for_each(|info| { - order_book - .push(info.trade_account, info.order_info) - .expect("Failed to push to order_book"); - }); - let mut swaps = Vec::new(); - while let Some((to, from)) = order_book.pop() { - swaps.push((to, from)); - if swaps.len() >= batch_size { - break; - } - } - let swaps_size = swaps.len(); - - let mut to_swap = vec![]; - let start = account_group * swaps_size as usize; - let end = account_group * swaps_size as usize + batch_size as usize; - for (signer, swap, profit) in izip!( - signers[start..end].iter(), - swaps, - profit_pubkeys[start..end].iter(), - ) { - to_swap.push((signer, swap, profit)); - } - account_group = (account_group + 1) % account_groups as usize; - - let (blockhash, _fee_calculator, _last_valid_slot) = client - .get_recent_blockhash_with_commitment(CommitmentConfig::processed()) - .expect("Failed to get blockhash"); - let to_swap_txs: Vec<_> = to_swap - .par_iter() - .map(|(signer, swap, profit)| { - let s: &Keypair = signer; - let owner = &signer.pubkey(); - let instruction = exchange_instruction::swap_request( - owner, - &swap.0.pubkey, - &swap.1.pubkey, - profit, - ); - let message = Message::new(&[instruction], Some(&s.pubkey())); - Transaction::new(&[s], message, blockhash) - }) - .collect(); - - txs += to_swap_txs.len() as u64; - total_txs += to_swap_txs.len() as u64; - total_elapsed = start_time.elapsed(); - let duration = now.elapsed(); - if duration_as_s(&duration) >= 1_f32 { - now = Instant::now(); - let tps = txs as f32 / duration_as_s(&duration); - info!( - "Swapper {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s", - tps, - txs, - total_txs, - total_elapsed.as_secs(), - ); - txs = 0; - } - - datapoint_info!("bench-exchange-swaps", ("count", to_swap_txs.len(), i64)); - - let chunks: Vec<_> = to_swap_txs.chunks(chunk_size).collect(); - { - let mut shared_txs_wl = shared_txs.write().unwrap(); - for chunk in chunks { - shared_txs_wl.push_back(chunk.to_vec()); - } - } - // Throttle the swapper so it doesn't try to catchup unbridled - sleep(Duration::from_millis(transfer_delay / 2)); - } - - if exit_signal.load(Ordering::Relaxed) { - break 'outer; - } - } - info!( - "Swapper sent {} at {:9.2} TPS", - total_txs, - total_txs as f32 / duration_as_s(&total_elapsed) - ); - assert_eq!( - order_book.get_num_outstanding().0 + order_book.get_num_outstanding().1, - 0 - ); -} - -#[allow(clippy::too_many_arguments)] -fn trader( - exit_signal: &Arc, - sender: &Sender>, - shared_txs: &SharedTransactions, - signers: &[Arc], - srcs: &[Pubkey], - transfer_delay: u64, - batch_size: usize, - chunk_size: usize, - account_groups: usize, - client: &Arc, -) where - T: Client, -{ - // TODO Hard coded for now - let pair = AssetPair::default(); - let tokens = 1; - let price = 1000; - let mut account_group: usize = 0; - - let mut txs = 0; - let mut total_txs = 0; - let mut now = Instant::now(); - let start_time = now; - let mut total_elapsed = start_time.elapsed(); - - loop { - let trade_keys = generate_keypairs(batch_size as u64); - - let mut trades = vec![]; - let mut trade_infos = vec![]; - let start = account_group * batch_size as usize; - let end = account_group * batch_size as usize + batch_size as usize; - let mut side = OrderSide::Ask; - for (signer, trade, src) in izip!( - signers[start..end].iter(), - trade_keys, - srcs[start..end].iter(), - ) { - side = if side == OrderSide::Ask { - OrderSide::Bid - } else { - OrderSide::Ask - }; - let order_info = OrderInfo { - /// Owner of the trade order - owner: Pubkey::default(), // don't care - side, - pair, - tokens, - price, - tokens_settled: 0, - }; - trade_infos.push(TradeInfo { - trade_account: trade.pubkey(), - order_info, - }); - trades.push((signer, trade, side, src)); - } - account_group = (account_group + 1) % account_groups as usize; - - let (blockhash, _fee_calculator, _last_valid_slot) = client - .get_recent_blockhash_with_commitment(CommitmentConfig::processed()) - .expect("Failed to get blockhash"); - - trades.chunks(chunk_size).for_each(|chunk| { - let trades_txs: Vec<_> = chunk - .par_iter() - .map(|(owner, trade, side, src)| { - let owner_pubkey = &owner.pubkey(); - let trade_pubkey = &trade.pubkey(); - let space = mem::size_of::() as u64; - let instructions = [ - system_instruction::create_account( - owner_pubkey, - trade_pubkey, - 1, - space, - &id(), - ), - exchange_instruction::trade_request( - owner_pubkey, - trade_pubkey, - *side, - pair, - tokens, - price, - src, - ), - ]; - let message = Message::new(&instructions, Some(owner_pubkey)); - Transaction::new(&[owner.as_ref(), trade], message, blockhash) - }) - .collect(); - - { - txs += chunk_size as u64; - total_txs += chunk_size as u64; - total_elapsed = start_time.elapsed(); - let duration = now.elapsed(); - if duration_as_s(&duration) >= 1_f32 { - now = Instant::now(); - let tps = txs as f32 / duration_as_s(&duration); - info!( - "Trader {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s", - tps, - txs, - total_txs, - total_elapsed.as_secs(), - ); - txs = 0; - } - - datapoint_info!("bench-exchange-trades", ("count", trades_txs.len(), i64)); - - { - let mut shared_txs_wl = shared_txs - .write() - .expect("Failed to send tx to transfer threads"); - shared_txs_wl.push_back(trades_txs); - } - } - if transfer_delay > 0 { - sleep(Duration::from_millis(transfer_delay)); - } - }); - - if exit_signal.load(Ordering::Relaxed) { - info!( - "Trader sent {} at {:9.2} TPS", - total_txs, - total_txs as f32 / duration_as_s(&total_elapsed) - ); - return; - } - - // TODO chunk the trade infos and send them when the batch is sent - sender - .send(trade_infos) - .expect("Failed to send trades to swapper"); - } -} - -fn verify_transaction(sync_client: &T, tx: &Transaction) -> bool -where - T: SyncClient + ?Sized, -{ - for s in &tx.signatures { - if let Ok(Some(r)) = - sync_client.get_signature_status_with_commitment(s, CommitmentConfig::processed()) - { - match r { - Ok(_) => { - return true; - } - Err(e) => { - info!("error: {:?}", e); - } - } - } - } - false -} - -fn verify_funding_transfer( - client: &T, - tx: &Transaction, - amount: u64, -) -> bool { - if verify_transaction(client, tx) { - for a in &tx.message().account_keys[1..] { - if client - .get_balance_with_commitment(a, CommitmentConfig::processed()) - .unwrap_or(0) - >= amount - { - return true; - } - } - } - false -} - -pub fn fund_keys(client: &T, source: &Keypair, dests: &[Arc], lamports: u64) { - let total = lamports * (dests.len() as u64 + 1); - let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)]; - let mut notfunded: Vec<&Arc> = dests.iter().collect(); - - info!( - " Funding {} keys with {} lamports each", - dests.len(), - lamports - ); - while !notfunded.is_empty() { - if funded.is_empty() { - panic!("No funded accounts left to fund remaining"); - } - let mut new_funded: Vec<(&Keypair, u64)> = vec![]; - let mut to_fund = vec![]; - debug!(" Creating from... {}", funded.len()); - for f in &mut funded { - let max_units = cmp::min( - cmp::min(notfunded.len() as u64, MAX_TRANSFERS_PER_TX), - (f.1 - lamports) / lamports, - ); - if max_units == 0 { - continue; - } - let per_unit = ((f.1 - lamports) / lamports / max_units) * lamports; - f.1 -= per_unit * max_units; - let start = notfunded.len() - max_units as usize; - let moves: Vec<_> = notfunded[start..] - .iter() - .map(|k| (k.pubkey(), per_unit)) - .collect(); - notfunded[start..] - .iter() - .for_each(|k| new_funded.push((k, per_unit))); - notfunded.truncate(start); - if !moves.is_empty() { - to_fund.push((f.0, moves)); - } - } - - to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - #[allow(clippy::clone_double_ref)] // sigh - let mut to_fund_txs: Vec<_> = chunk - .par_iter() - .map(|(k, m)| { - let instructions = system_instruction::transfer_many(&k.pubkey(), m); - let message = Message::new(&instructions, Some(&k.pubkey())); - (k.clone(), Transaction::new_unsigned(message)) - }) - .collect(); - - let mut retries = 0; - let amount = chunk[0].1[0].1; - while !to_fund_txs.is_empty() { - let receivers: usize = to_fund_txs - .iter() - .map(|(_, tx)| tx.message().instructions.len()) - .sum(); - - debug!( - " {} to {} in {} txs", - if retries == 0 { - " Transferring" - } else { - " Retrying" - }, - receivers, - to_fund_txs.len(), - ); - - let (blockhash, _fee_calculator, _last_valid_slot) = client - .get_recent_blockhash_with_commitment(CommitmentConfig::processed()) - .expect("blockhash"); - to_fund_txs.par_iter_mut().for_each(|(k, tx)| { - tx.sign(&[*k], blockhash); - }); - to_fund_txs.iter().for_each(|(_, tx)| { - client.async_send_transaction(tx.clone()).expect("transfer"); - }); - - let mut waits = 0; - loop { - sleep(Duration::from_millis(200)); - to_fund_txs.retain(|(_, tx)| !verify_funding_transfer(client, tx, amount)); - if to_fund_txs.is_empty() { - break; - } - debug!( - " {} transactions outstanding, {:?} waits", - to_fund_txs.len(), - waits - ); - waits += 1; - if waits >= 5 { - break; - } - } - if !to_fund_txs.is_empty() { - retries += 1; - debug!(" Retry {:?}", retries); - if retries >= 10 { - error!("fund_keys: Too many retries ({}), give up", retries); - exit(1); - } - } - } - }); - funded.append(&mut new_funded); - funded.retain(|(k, b)| { - client - .get_balance_with_commitment(&k.pubkey(), CommitmentConfig::processed()) - .unwrap_or(0) - > lamports - && *b > lamports - }); - debug!(" Funded: {} left: {}", funded.len(), notfunded.len()); - } -} - -pub fn create_token_accounts( - client: &T, - signers: &[Arc], - accounts: &[Keypair], -) { - let mut notfunded: Vec<(&Arc, &Keypair)> = signers.iter().zip(accounts).collect(); - - while !notfunded.is_empty() { - notfunded.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - let mut to_create_txs: Vec<_> = chunk - .par_iter() - .map(|(from_keypair, new_keypair)| { - let owner_pubkey = &from_keypair.pubkey(); - let space = mem::size_of::() as u64; - let create_ix = system_instruction::create_account( - owner_pubkey, - &new_keypair.pubkey(), - 1, - space, - &id(), - ); - let request_ix = - exchange_instruction::account_request(owner_pubkey, &new_keypair.pubkey()); - let message = Message::new(&[create_ix, request_ix], Some(owner_pubkey)); - ( - (from_keypair, new_keypair), - Transaction::new_unsigned(message), - ) - }) - .collect(); - - let accounts: usize = to_create_txs - .iter() - .map(|(_, tx)| tx.message().instructions.len() / 2) - .sum(); - - debug!( - " Creating {} accounts in {} txs", - accounts, - to_create_txs.len(), - ); - - let mut retries = 0; - while !to_create_txs.is_empty() { - let (blockhash, _fee_calculator, _last_valid_slot) = client - .get_recent_blockhash_with_commitment(CommitmentConfig::processed()) - .expect("Failed to get blockhash"); - to_create_txs - .par_iter_mut() - .for_each(|((from_keypair, to_keypair), tx)| { - tx.sign(&[from_keypair.as_ref(), to_keypair], blockhash); - }); - to_create_txs.iter().for_each(|(_, tx)| { - client.async_send_transaction(tx.clone()).expect("transfer"); - }); - - let mut waits = 0; - while !to_create_txs.is_empty() { - sleep(Duration::from_millis(200)); - to_create_txs.retain(|(_, tx)| !verify_transaction(client, tx)); - if to_create_txs.is_empty() { - break; - } - info!( - " {} transactions outstanding, waits {:?}", - to_create_txs.len(), - waits - ); - waits += 1; - if waits >= 5 { - break; - } - } - - if !to_create_txs.is_empty() { - retries += 1; - info!(" Retry {:?} {} txes left", retries, to_create_txs.len()); - if retries >= 20 { - error!( - "create_token_accounts: Too many retries ({}), give up", - retries - ); - exit(1); - } - } - } - }); - - let mut new_notfunded: Vec<(&Arc, &Keypair)> = vec![]; - for f in ¬funded { - if client - .get_balance_with_commitment(&f.1.pubkey(), CommitmentConfig::processed()) - .unwrap_or(0) - == 0 - { - new_notfunded.push(*f) - } - } - notfunded = new_notfunded; - debug!(" Left: {}", notfunded.len()); - } -} - -fn compute_and_report_stats(maxes: &Arc>>, total_txs_sent: u64) { - let mut max_txs = 0; - let mut max_elapsed = Duration::new(0, 0); - info!("| Max TPS | Total Transactions"); - info!("+---------------+--------------------"); - - for (_sock, stats) in maxes.read().unwrap().iter() { - let maybe_flag = match stats.txs { - 0 => "!!!!!", - _ => "", - }; - - info!("| {:13.2} | {} {}", stats.tps, stats.txs, maybe_flag); - - if stats.elapsed > max_elapsed { - max_elapsed = stats.elapsed; - } - if stats.txs > max_txs { - max_txs = stats.txs; - } - } - info!("+---------------+--------------------"); - - if max_txs >= total_txs_sent { - info!( - "Warning: Average TPS might be under reported, there were no txs sent for a portion of the duration" - ); - max_txs = total_txs_sent; - } - info!( - "{} txs outstanding when test ended (lag) ({:.2}%)", - total_txs_sent - max_txs, - (total_txs_sent - max_txs) as f64 / total_txs_sent as f64 * 100_f64 - ); - info!( - "\tAverage TPS: {:.2}", - max_txs as f32 / max_elapsed.as_secs() as f32 - ); -} - -fn generate_keypairs(num: u64) -> Vec { - let mut seed = [0_u8; 32]; - seed.copy_from_slice(Keypair::new().pubkey().as_ref()); - let mut rnd = GenKeys::new(seed); - rnd.gen_n_keypairs(num) -} - -pub fn airdrop_lamports( - client: &T, - faucet_addr: &SocketAddr, - id: &Keypair, - amount: u64, -) { - let balance = client.get_balance_with_commitment(&id.pubkey(), CommitmentConfig::processed()); - let balance = balance.unwrap_or(0); - if balance >= amount { - return; - } - - let amount_to_drop = amount - balance; - - info!( - "Airdropping {:?} lamports from {} for {}", - amount_to_drop, - faucet_addr, - id.pubkey(), - ); - - let mut tries = 0; - loop { - let (blockhash, _fee_calculator, _last_valid_slot) = client - .get_recent_blockhash_with_commitment(CommitmentConfig::processed()) - .expect("Failed to get blockhash"); - match request_airdrop_transaction(faucet_addr, &id.pubkey(), amount_to_drop, blockhash) { - Ok(transaction) => { - let signature = client.async_send_transaction(transaction).unwrap(); - - for _ in 0..30 { - if let Ok(Some(_)) = client.get_signature_status_with_commitment( - &signature, - CommitmentConfig::processed(), - ) { - break; - } - sleep(Duration::from_millis(100)); - } - if client - .get_balance_with_commitment(&id.pubkey(), CommitmentConfig::processed()) - .unwrap_or(0) - >= amount - { - break; - } - } - Err(err) => { - panic!( - "Error requesting airdrop: {:?} to addr: {:?} amount: {}", - err, faucet_addr, amount - ); - } - }; - debug!(" Retry..."); - tries += 1; - if tries > 50 { - error!("airdrop_lamports: Too many retries ({}), give up", tries); - exit(1); - } - sleep(Duration::from_secs(2)); - } -} diff --git a/bench-exchange/src/cli.rs b/bench-exchange/src/cli.rs deleted file mode 100644 index 21e8f39db..000000000 --- a/bench-exchange/src/cli.rs +++ /dev/null @@ -1,221 +0,0 @@ -use clap::{crate_description, crate_name, value_t, App, Arg, ArgMatches}; -use solana_core::gen_keys::GenKeys; -use solana_faucet::faucet::FAUCET_PORT; -use solana_sdk::signature::{read_keypair_file, Keypair}; -use std::net::SocketAddr; -use std::process::exit; -use std::time::Duration; - -pub struct Config { - pub entrypoint_addr: SocketAddr, - pub faucet_addr: SocketAddr, - pub identity: Keypair, - pub threads: usize, - pub num_nodes: usize, - pub duration: Duration, - pub transfer_delay: u64, - pub fund_amount: u64, - pub batch_size: usize, - pub chunk_size: usize, - pub account_groups: usize, - pub client_ids_and_stake_file: String, - pub write_to_client_file: bool, - pub read_from_client_file: bool, -} - -impl Default for Config { - fn default() -> Self { - Self { - entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)), - faucet_addr: SocketAddr::from(([127, 0, 0, 1], FAUCET_PORT)), - identity: Keypair::new(), - num_nodes: 1, - threads: 4, - duration: Duration::new(u64::max_value(), 0), - transfer_delay: 0, - fund_amount: 100_000, - batch_size: 100, - chunk_size: 100, - account_groups: 100, - client_ids_and_stake_file: String::new(), - write_to_client_file: false, - read_from_client_file: false, - } - } -} - -pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> { - App::new(crate_name!()) - .about(crate_description!()) - .version(version) - .arg( - Arg::with_name("entrypoint") - .short("n") - .long("entrypoint") - .value_name("HOST:PORT") - .takes_value(true) - .required(false) - .default_value("127.0.0.1:8001") - .help("Cluster entry point; defaults to 127.0.0.1:8001"), - ) - .arg( - Arg::with_name("faucet") - .short("d") - .long("faucet") - .value_name("HOST:PORT") - .takes_value(true) - .required(false) - .default_value("127.0.0.1:9900") - .help("Location of the faucet; defaults to 127.0.0.1:9900"), - ) - .arg( - Arg::with_name("identity") - .short("i") - .long("identity") - .value_name("PATH") - .takes_value(true) - .help("File containing a client identity (keypair)"), - ) - .arg( - Arg::with_name("threads") - .long("threads") - .value_name("") - .takes_value(true) - .required(false) - .default_value("1") - .help("Number of threads submitting transactions"), - ) - .arg( - Arg::with_name("num-nodes") - .long("num-nodes") - .value_name("NUM") - .takes_value(true) - .required(false) - .default_value("1") - .help("Wait for NUM nodes to converge"), - ) - .arg( - Arg::with_name("duration") - .long("duration") - .value_name("SECS") - .takes_value(true) - .default_value("60") - .help("Seconds to run benchmark, then exit; default is forever"), - ) - .arg( - Arg::with_name("transfer-delay") - .long("transfer-delay") - .value_name("") - .takes_value(true) - .required(false) - .default_value("0") - .help("Delay between each chunk"), - ) - .arg( - Arg::with_name("fund-amount") - .long("fund-amount") - .value_name("") - .takes_value(true) - .required(false) - .default_value("100000") - .help("Number of lamports to fund to each signer"), - ) - .arg( - Arg::with_name("batch-size") - .long("batch-size") - .value_name("") - .takes_value(true) - .required(false) - .default_value("1000") - .help("Number of transactions before the signer rolls over"), - ) - .arg( - Arg::with_name("chunk-size") - .long("chunk-size") - .value_name("") - .takes_value(true) - .required(false) - .default_value("500") - .help("Number of transactions to generate and send at a time"), - ) - .arg( - Arg::with_name("account-groups") - .long("account-groups") - .value_name("") - .takes_value(true) - .required(false) - .default_value("10") - .help("Number of account groups to cycle for each batch"), - ) - .arg( - Arg::with_name("write-client-keys") - .long("write-client-keys") - .value_name("FILENAME") - .takes_value(true) - .help("Generate client keys and stakes and write the list to YAML file"), - ) - .arg( - Arg::with_name("read-client-keys") - .long("read-client-keys") - .value_name("FILENAME") - .takes_value(true) - .help("Read client keys and stakes from the YAML file"), - ) -} - -#[allow(clippy::field_reassign_with_default)] -pub fn extract_args(matches: &ArgMatches) -> Config { - let mut args = Config::default(); - - args.entrypoint_addr = solana_net_utils::parse_host_port( - matches.value_of("entrypoint").unwrap(), - ) - .unwrap_or_else(|e| { - eprintln!("failed to parse entrypoint address: {}", e); - exit(1) - }); - - args.faucet_addr = solana_net_utils::parse_host_port(matches.value_of("faucet").unwrap()) - .unwrap_or_else(|e| { - eprintln!("failed to parse faucet address: {}", e); - exit(1) - }); - - if matches.is_present("identity") { - args.identity = read_keypair_file(matches.value_of("identity").unwrap()) - .expect("can't read client identity"); - } else { - args.identity = { - let seed = [42_u8; 32]; - let mut rnd = GenKeys::new(seed); - rnd.gen_keypair() - }; - } - args.threads = value_t!(matches.value_of("threads"), usize).expect("Failed to parse threads"); - args.num_nodes = - value_t!(matches.value_of("num-nodes"), usize).expect("Failed to parse num-nodes"); - let duration = value_t!(matches.value_of("duration"), u64).expect("Failed to parse duration"); - args.duration = Duration::from_secs(duration); - args.transfer_delay = - value_t!(matches.value_of("transfer-delay"), u64).expect("Failed to parse transfer-delay"); - args.fund_amount = - value_t!(matches.value_of("fund-amount"), u64).expect("Failed to parse fund-amount"); - args.batch_size = - value_t!(matches.value_of("batch-size"), usize).expect("Failed to parse batch-size"); - args.chunk_size = - value_t!(matches.value_of("chunk-size"), usize).expect("Failed to parse chunk-size"); - args.account_groups = value_t!(matches.value_of("account-groups"), usize) - .expect("Failed to parse account-groups"); - - if let Some(s) = matches.value_of("write-client-keys") { - args.write_to_client_file = true; - args.client_ids_and_stake_file = s.to_string(); - } - - if let Some(s) = matches.value_of("read-client-keys") { - assert!(!args.write_to_client_file); - args.read_from_client_file = true; - args.client_ids_and_stake_file = s.to_string(); - } - args -} diff --git a/bench-exchange/src/lib.rs b/bench-exchange/src/lib.rs deleted file mode 100644 index dbb7feff0..000000000 --- a/bench-exchange/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod bench; -pub mod cli; -mod order_book; diff --git a/bench-exchange/src/main.rs b/bench-exchange/src/main.rs deleted file mode 100644 index 8986a67e6..000000000 --- a/bench-exchange/src/main.rs +++ /dev/null @@ -1,83 +0,0 @@ -#![allow(clippy::integer_arithmetic)] -pub mod bench; -mod cli; -pub mod order_book; - -use crate::bench::{airdrop_lamports, create_client_accounts_file, do_bench_exchange, Config}; -use log::*; -use solana_gossip::gossip_service::{discover_cluster, get_multi_client}; -use solana_sdk::signature::Signer; - -fn main() { - solana_logger::setup(); - solana_metrics::set_panic_hook("bench-exchange"); - - let matches = cli::build_args(solana_version::version!()).get_matches(); - let cli_config = cli::extract_args(&matches); - - let cli::Config { - entrypoint_addr, - faucet_addr, - identity, - threads, - num_nodes, - duration, - transfer_delay, - fund_amount, - batch_size, - chunk_size, - account_groups, - client_ids_and_stake_file, - write_to_client_file, - read_from_client_file, - .. - } = cli_config; - - let config = Config { - identity, - threads, - duration, - transfer_delay, - fund_amount, - batch_size, - chunk_size, - account_groups, - client_ids_and_stake_file, - read_from_client_file, - }; - - if write_to_client_file { - create_client_accounts_file( - &config.client_ids_and_stake_file, - config.batch_size, - config.account_groups, - config.fund_amount, - ); - } else { - info!("Connecting to the cluster"); - let nodes = discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|_| { - panic!("Failed to discover nodes"); - }); - - let (client, num_clients) = get_multi_client(&nodes); - - info!("{} nodes found", num_clients); - if num_clients < num_nodes { - panic!("Error: Insufficient nodes discovered"); - } - - if !read_from_client_file { - info!("Funding keypair: {}", config.identity.pubkey()); - - let accounts_in_groups = batch_size * account_groups; - const NUM_SIGNERS: u64 = 2; - airdrop_lamports( - &client, - &faucet_addr, - &config.identity, - fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS, - ); - } - do_bench_exchange(vec![client], config); - } -} diff --git a/bench-exchange/src/order_book.rs b/bench-exchange/src/order_book.rs deleted file mode 100644 index d4c06743c..000000000 --- a/bench-exchange/src/order_book.rs +++ /dev/null @@ -1,134 +0,0 @@ -use itertools::EitherOrBoth::{Both, Left, Right}; -use itertools::Itertools; -use log::*; -use solana_exchange_program::exchange_state::*; -use solana_sdk::pubkey::Pubkey; -use std::cmp::Ordering; -use std::collections::BinaryHeap; -use std::{error, fmt}; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ToOrder { - pub pubkey: Pubkey, - pub info: OrderInfo, -} - -impl Ord for ToOrder { - fn cmp(&self, other: &Self) -> Ordering { - other.info.price.cmp(&self.info.price) - } -} -impl PartialOrd for ToOrder { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct FromOrder { - pub pubkey: Pubkey, - pub info: OrderInfo, -} - -impl Ord for FromOrder { - fn cmp(&self, other: &Self) -> Ordering { - self.info.price.cmp(&other.info.price) - } -} -impl PartialOrd for FromOrder { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -#[derive(Default)] -pub struct OrderBook { - // TODO scale to x token types - to_ab: BinaryHeap, - from_ab: BinaryHeap, -} -impl fmt::Display for OrderBook { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "+-Order Book--------------------------+-------------------------------------+" - )?; - for (i, it) in self - .to_ab - .iter() - .zip_longest(self.from_ab.iter()) - .enumerate() - { - match it { - Both(to, from) => writeln!( - f, - "| T AB {:8} for {:8}/{:8} | F AB {:8} for {:8}/{:8} |{}", - to.info.tokens, - SCALER, - to.info.price, - from.info.tokens, - SCALER, - from.info.price, - i - )?, - Left(to) => writeln!( - f, - "| T AB {:8} for {:8}/{:8} | |{}", - to.info.tokens, SCALER, to.info.price, i - )?, - Right(from) => writeln!( - f, - "| | F AB {:8} for {:8}/{:8} |{}", - from.info.tokens, SCALER, from.info.price, i - )?, - } - } - write!( - f, - "+-------------------------------------+-------------------------------------+" - )?; - Ok(()) - } -} - -impl OrderBook { - // TODO - // pub fn cancel(&mut self, pubkey: Pubkey) -> Result<(), Box> { - // Ok(()) - // } - pub fn push(&mut self, pubkey: Pubkey, info: OrderInfo) -> Result<(), Box> { - check_trade(info.side, info.tokens, info.price)?; - match info.side { - OrderSide::Ask => { - self.to_ab.push(ToOrder { pubkey, info }); - } - OrderSide::Bid => { - self.from_ab.push(FromOrder { pubkey, info }); - } - } - Ok(()) - } - pub fn pop(&mut self) -> Option<(ToOrder, FromOrder)> { - if let Some(pair) = Self::pop_pair(&mut self.to_ab, &mut self.from_ab) { - return Some(pair); - } - None - } - pub fn get_num_outstanding(&self) -> (usize, usize) { - (self.to_ab.len(), self.from_ab.len()) - } - - fn pop_pair( - to_ab: &mut BinaryHeap, - from_ab: &mut BinaryHeap, - ) -> Option<(ToOrder, FromOrder)> { - let to = to_ab.peek()?; - let from = from_ab.peek()?; - if from.info.price < to.info.price { - debug!("Trade not viable"); - return None; - } - let to = to_ab.pop()?; - let from = from_ab.pop()?; - Some((to, from)) - } -} diff --git a/bench-exchange/tests/bench_exchange.rs b/bench-exchange/tests/bench_exchange.rs deleted file mode 100644 index 0cba65a7f..000000000 --- a/bench-exchange/tests/bench_exchange.rs +++ /dev/null @@ -1,113 +0,0 @@ -use log::*; -use solana_bench_exchange::bench::{airdrop_lamports, do_bench_exchange, Config}; -use solana_core::validator::ValidatorConfig; -use solana_exchange_program::{ - exchange_processor::process_instruction, id, solana_exchange_program, -}; -use solana_faucet::faucet::run_local_faucet_with_port; -use solana_gossip::gossip_service::{discover_cluster, get_multi_client}; -use solana_local_cluster::{ - local_cluster::{ClusterConfig, LocalCluster}, - validator_configs::make_identical_validator_configs, -}; -use solana_runtime::{bank::Bank, bank_client::BankClient}; -use solana_sdk::{ - genesis_config::create_genesis_config, - signature::{Keypair, Signer}, -}; -use std::{process::exit, sync::mpsc::channel, time::Duration}; - -#[test] -#[ignore] -fn test_exchange_local_cluster() { - solana_logger::setup(); - - const NUM_NODES: usize = 1; - - let config = Config { - identity: Keypair::new(), - duration: Duration::from_secs(1), - fund_amount: 100_000, - threads: 1, - transfer_delay: 20, // 15 - batch_size: 100, // 1000 - chunk_size: 10, // 200 - account_groups: 1, // 10 - ..Config::default() - }; - let Config { - fund_amount, - batch_size, - account_groups, - .. - } = config; - let accounts_in_groups = batch_size * account_groups; - - let cluster = LocalCluster::new(&mut ClusterConfig { - node_stakes: vec![100_000; NUM_NODES], - cluster_lamports: 100_000_000_000_000, - validator_configs: make_identical_validator_configs(&ValidatorConfig::default(), NUM_NODES), - native_instruction_processors: [solana_exchange_program!()].to_vec(), - ..ClusterConfig::default() - }); - - let faucet_keypair = Keypair::new(); - cluster.transfer( - &cluster.funding_keypair, - &faucet_keypair.pubkey(), - 2_000_000_000_000, - ); - - let (addr_sender, addr_receiver) = channel(); - run_local_faucet_with_port(faucet_keypair, addr_sender, Some(1_000_000_000_000), 0); - let faucet_addr = addr_receiver - .recv_timeout(Duration::from_secs(2)) - .expect("run_local_faucet") - .expect("faucet_addr"); - - info!("Connecting to the cluster"); - let nodes = - discover_cluster(&cluster.entry_point_info.gossip, NUM_NODES).unwrap_or_else(|err| { - error!("Failed to discover {} nodes: {:?}", NUM_NODES, err); - exit(1); - }); - - let (client, num_clients) = get_multi_client(&nodes); - - info!("clients: {}", num_clients); - assert!(num_clients >= NUM_NODES); - - const NUM_SIGNERS: u64 = 2; - airdrop_lamports( - &client, - &faucet_addr, - &config.identity, - fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS, - ); - - do_bench_exchange(vec![client], config); -} - -#[test] -fn test_exchange_bank_client() { - solana_logger::setup(); - let (genesis_config, identity) = create_genesis_config(100_000_000_000_000); - let mut bank = Bank::new(&genesis_config); - bank.add_builtin("exchange_program", id(), process_instruction); - let clients = vec![BankClient::new(bank)]; - - do_bench_exchange( - clients, - Config { - identity, - duration: Duration::from_secs(1), - fund_amount: 100_000, - threads: 1, - transfer_delay: 20, // 0; - batch_size: 100, // 1500; - chunk_size: 10, // 1500; - account_groups: 1, // 50; - ..Config::default() - }, - ); -} diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index ae495191d..a831c1345 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -18,7 +18,6 @@ serde_json = "1.0.64" serde_yaml = "0.8.17" solana-clap-utils = { path = "../clap-utils", version = "=1.8.0" } solana-cli-config = { path = "../cli-config", version = "=1.8.0" } -solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" } solana-ledger = { path = "../ledger", version = "=1.8.0" } solana-logger = { path = "../logger", version = "=1.8.0" } solana-runtime = { path = "../runtime", version = "=1.8.0" } diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 2fb749833..33ad18202 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -1,9 +1,6 @@ //! A command-line executable for generating the chain's genesis config. #![allow(clippy::integer_arithmetic)] -#[macro_use] -extern crate solana_exchange_program; - use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg, ArgMatches}; use solana_clap_utils::{ input_parsers::{cluster_type_of, pubkey_of, pubkeys_of, unix_timestamp_from_rfc3339_datetime}, @@ -490,14 +487,8 @@ fn main() -> Result<(), Box> { matches.is_present("enable_warmup_epochs"), ); - let native_instruction_processors = if cluster_type == ClusterType::Development { - vec![solana_exchange_program!()] - } else { - vec![] - }; - let mut genesis_config = GenesisConfig { - native_instruction_processors, + native_instruction_processors: vec![], ticks_per_slot, poh_config, fee_rate_governor, diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index ea45319b7..7636e565d 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -21,7 +21,6 @@ solana-config-program = { path = "../programs/config", version = "=1.8.0" } solana-core = { path = "../core", version = "=1.8.0" } solana-client = { path = "../client", version = "=1.8.0" } solana-download-utils = { path = "../download-utils", version = "=1.8.0" } -solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" } solana-faucet = { path = "../faucet", version = "=1.8.0" } solana-gossip = { path = "../gossip", version = "=1.8.0" } solana-ledger = { path = "../ledger", version = "=1.8.0" } diff --git a/net/net.sh b/net/net.sh index 87930fd51..b3da0ad32 100755 --- a/net/net.sh +++ b/net/net.sh @@ -20,7 +20,6 @@ usage() { Valid client types are: idle bench-tps - bench-exchange User can optionally provide extraArgs that are transparently supplied to the client program as command line parameters. For example, @@ -307,7 +306,6 @@ startBootstrapLeader() { \"$internalNodesLamports\" \ $nodeIndex \ ${#clientIpList[@]} \"$benchTpsExtraArgs\" \ - ${#clientIpList[@]} \"$benchExchangeExtraArgs\" \ \"$genesisOptions\" \ \"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority\" \ \"$gpuMode\" \ @@ -379,7 +377,6 @@ startNode() { \"$internalNodesLamports\" \ $nodeIndex \ ${#clientIpList[@]} \"$benchTpsExtraArgs\" \ - ${#clientIpList[@]} \"$benchExchangeExtraArgs\" \ \"$genesisOptions\" \ \"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority\" \ \"$gpuMode\" \ @@ -409,7 +406,7 @@ startClient() { startCommon "$ipAddress" ssh "${sshOptions[@]}" -f "$ipAddress" \ "./solana/net/remote/remote-client.sh $deployMethod $entrypointIp \ - $clientToRun \"$RUST_LOG\" \"$benchTpsExtraArgs\" \"$benchExchangeExtraArgs\" $clientIndex" + $clientToRun \"$RUST_LOG\" \"$benchTpsExtraArgs\" $clientIndex" ) >> "$logFile" 2>&1 || { cat "$logFile" echo "^^^ +++" @@ -421,8 +418,6 @@ startClients() { for ((i=0; i < "$numClients" && i < "$numClientsRequested"; i++)) do if [[ $i -lt "$numBenchTpsClients" ]]; then startClient "${clientIpList[$i]}" "solana-bench-tps" "$i" - elif [[ $i -lt $((numBenchTpsClients + numBenchExchangeClients)) ]]; then - startClient "${clientIpList[$i]}" "solana-bench-exchange" $((i-numBenchTpsClients)) else startClient "${clientIpList[$i]}" "idle" fi @@ -767,9 +762,7 @@ updatePlatforms= nodeAddress= numIdleClients=0 numBenchTpsClients=0 -numBenchExchangeClients=0 benchTpsExtraArgs= -benchExchangeExtraArgs= failOnValidatorBootupFailure=true genesisOptions= numValidatorsRequested= @@ -977,10 +970,6 @@ while getopts "h?T:t:o:f:rc:Fn:i:d" opt "${shortArgs[@]}"; do numBenchTpsClients=$numClients benchTpsExtraArgs=$extraArgs ;; - bench-exchange) - numBenchExchangeClients=$numClients - benchExchangeExtraArgs=$extraArgs - ;; *) echo "Unknown client type: $clientType" exit 1 @@ -1013,7 +1002,7 @@ if [[ -n $numValidatorsRequested ]]; then fi numClients=${#clientIpList[@]} -numClientsRequested=$((numBenchTpsClients + numBenchExchangeClients + numIdleClients)) +numClientsRequested=$((numBenchTpsClients + numIdleClients)) if [[ "$numClientsRequested" -eq 0 ]]; then numBenchTpsClients=$numClients numClientsRequested=$numClients diff --git a/net/remote/remote-client.sh b/net/remote/remote-client.sh index 4ac7bc7c8..8d8d902e4 100755 --- a/net/remote/remote-client.sh +++ b/net/remote/remote-client.sh @@ -10,8 +10,7 @@ if [[ -n $4 ]]; then export RUST_LOG="$4" fi benchTpsExtraArgs="$5" -benchExchangeExtraArgs="$6" -clientIndex="$7" +clientIndex="$6" missing() { echo "Error: $1 not specified" @@ -57,23 +56,6 @@ solana-bench-tps) --read-client-keys ./client-accounts.yml \ " ;; -solana-bench-exchange) - solana-keygen new --no-passphrase -fso bench.keypair - net/scripts/rsync-retry.sh -vPrc \ - "$entrypointIp":~/solana/config/bench-exchange"$clientIndex".yml ./client-accounts.yml - clientCommand="\ - solana-bench-exchange \ - --entrypoint $entrypointIp:8001 \ - --faucet $entrypointIp:9900 \ - --threads $threadCount \ - --batch-size 1000 \ - --fund-amount 20000 \ - --duration 7500 \ - --identity bench.keypair \ - $benchExchangeExtraArgs \ - --read-client-keys ./client-accounts.yml \ - " - ;; idle) # Add the faucet keypair to idle clients for convenience net/scripts/rsync-retry.sh -vPrc \ diff --git a/net/remote/remote-node.sh b/net/remote/remote-node.sh index d302a0d6e..efbebe3e3 100755 --- a/net/remote/remote-node.sh +++ b/net/remote/remote-node.sh @@ -20,15 +20,13 @@ internalNodesLamports="${11}" nodeIndex="${12}" numBenchTpsClients="${13}" benchTpsExtraArgs="${14}" -numBenchExchangeClients="${15}" -benchExchangeExtraArgs="${16}" -genesisOptions="${17}" -extraNodeArgs="${18}" -gpuMode="${19:-auto}" -maybeWarpSlot="${20}" -waitForNodeInit="${21}" -extraPrimordialStakes="${22:=0}" -tmpfsAccounts="${23:false}" +genesisOptions="${15}" +extraNodeArgs="${16}" +gpuMode="${17:-auto}" +maybeWarpSlot="${18}" +waitForNodeInit="${19}" +extraPrimordialStakes="${20:=0}" +tmpfsAccounts="${21:false}" set +x missing() { @@ -194,13 +192,6 @@ EOF tail -n +2 -q config/bench-tps"$i".yml >> config/client-accounts.yml echo "" >> config/client-accounts.yml done - for i in $(seq 0 $((numBenchExchangeClients-1))); do - # shellcheck disable=SC2086 # Do not want to quote $benchExchangeExtraArgs - solana-bench-exchange --batch-size 1000 --fund-amount 20000 \ - --write-client-keys config/bench-exchange"$i".yml $benchExchangeExtraArgs - tail -n +2 -q config/bench-exchange"$i".yml >> config/client-accounts.yml - echo "" >> config/client-accounts.yml - done if [[ -f $externalPrimordialAccountsFile ]]; then cat "$externalPrimordialAccountsFile" >> config/validator-balances.yml fi diff --git a/programs/exchange/Cargo.toml b/programs/exchange/Cargo.toml deleted file mode 100644 index d38ae2512..000000000 --- a/programs/exchange/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "solana-exchange-program" -version = "1.8.0" -description = "Solana Exchange program" -authors = ["Solana Maintainers "] -repository = "https://github.com/solana-labs/solana" -license = "Apache-2.0" -homepage = "https://solana.com/" -documentation = "https://docs.rs/solana-exchange-program" -edition = "2018" - -[dependencies] -bincode = "1.3.3" -log = "0.4.14" -num-derive = { version = "0.3" } -num-traits = { version = "0.2" } -serde = "1.0.126" -serde_derive = "1.0.103" -solana-logger = { path = "../../logger", version = "=1.8.0" } -solana-metrics = { path = "../../metrics", version = "=1.8.0" } -solana-sdk = { path = "../../sdk", version = "=1.8.0" } -thiserror = "1.0" - -[dev-dependencies] -solana-runtime = { path = "../../runtime", version = "=1.8.0" } - -[lib] -crate-type = ["lib", "cdylib"] -name = "solana_exchange_program" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/exchange/src/exchange_instruction.rs b/programs/exchange/src/exchange_instruction.rs deleted file mode 100644 index 806c112f6..000000000 --- a/programs/exchange/src/exchange_instruction.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Exchange program - -use crate::exchange_state::*; -use crate::id; -use serde_derive::{Deserialize, Serialize}; -use solana_sdk::instruction::{AccountMeta, Instruction}; -use solana_sdk::pubkey::Pubkey; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct OrderRequestInfo { - /// Side of market of the order (bid/ask) - pub side: OrderSide, - - /// Token pair to trade - pub pair: AssetPair, - - /// Number of tokens to exchange; refers to the primary or the secondary depending on the order side - pub tokens: u64, - - /// The price ratio the primary price over the secondary price. The primary price is fixed - /// and equal to the variable `SCALER`. - pub price: u64, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub enum ExchangeInstruction { - /// New token account - /// key 0 - Signer - /// key 1 - New token account - AccountRequest, - - /// Transfer tokens between two accounts - /// key 0 - Account to transfer tokens to - /// key 1 - Account to transfer tokens from. This can be the exchange program itself, - /// the exchange has a limitless number of tokens it can transfer. - TransferRequest(Token, u64), - - /// Order request - /// key 0 - Signer - /// key 1 - Account in which to record the trade order - /// key 2 - Token account to source tokens from - OrderRequest(OrderRequestInfo), - - /// Order cancellation - /// key 0 - Signer - /// key 1 - Order to cancel - OrderCancellation, - - /// Trade swap request - /// key 0 - Signer - /// key 2 - 'To' trade order - /// key 3 - `From` trade order - /// key 6 - Token account in which to deposit the brokers profit from the swap. - SwapRequest, -} - -pub fn account_request(owner: &Pubkey, new: &Pubkey) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*owner, true), - AccountMeta::new(*new, false), - ]; - Instruction::new_with_bincode(id(), &ExchangeInstruction::AccountRequest, account_metas) -} - -pub fn transfer_request( - owner: &Pubkey, - to: &Pubkey, - from: &Pubkey, - token: Token, - tokens: u64, -) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*owner, true), - AccountMeta::new(*to, false), - AccountMeta::new(*from, false), - ]; - Instruction::new_with_bincode( - id(), - &ExchangeInstruction::TransferRequest(token, tokens), - account_metas, - ) -} - -pub fn trade_request( - owner: &Pubkey, - trade: &Pubkey, - side: OrderSide, - pair: AssetPair, - tokens: u64, - price: u64, - src_account: &Pubkey, -) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*owner, true), - AccountMeta::new(*trade, false), - AccountMeta::new(*src_account, false), - ]; - Instruction::new_with_bincode( - id(), - &ExchangeInstruction::OrderRequest(OrderRequestInfo { - side, - pair, - tokens, - price, - }), - account_metas, - ) -} - -pub fn order_cancellation(owner: &Pubkey, order: &Pubkey) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*owner, true), - AccountMeta::new(*order, false), - ]; - Instruction::new_with_bincode(id(), &ExchangeInstruction::OrderCancellation, account_metas) -} - -pub fn swap_request( - owner: &Pubkey, - to_trade: &Pubkey, - from_trade: &Pubkey, - profit_account: &Pubkey, -) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*owner, true), - AccountMeta::new(*to_trade, false), - AccountMeta::new(*from_trade, false), - AccountMeta::new(*profit_account, false), - ]; - Instruction::new_with_bincode(id(), &ExchangeInstruction::SwapRequest, account_metas) -} diff --git a/programs/exchange/src/exchange_processor.rs b/programs/exchange/src/exchange_processor.rs deleted file mode 100644 index ab63cf7ec..000000000 --- a/programs/exchange/src/exchange_processor.rs +++ /dev/null @@ -1,920 +0,0 @@ -//! Config processor - -use crate::exchange_instruction::*; -use crate::exchange_state::*; -use crate::faucet; -use log::*; -use num_derive::{FromPrimitive, ToPrimitive}; -use serde_derive::Serialize; -use solana_metrics::inc_new_counter_info; -use solana_sdk::{ - account::{ReadableAccount, WritableAccount}, - decode_error::DecodeError, - instruction::InstructionError, - keyed_account::KeyedAccount, - process_instruction::InvokeContext, - program_utils::limited_deserialize, - pubkey::Pubkey, -}; -use std::cmp; -use thiserror::Error; - -#[derive(Error, Debug, Serialize, Clone, PartialEq, FromPrimitive, ToPrimitive)] -pub enum ExchangeError { - #[error("Signer does not own account")] - SignerDoesNotOwnAccount, - #[error("Signer does not own order")] - SignerDoesNotOwnOrder, - #[error("The From account balance is too low")] - FromAccountBalanceTooLow, - #[error("Attmept operation on mismatched tokens")] - TokenMismatch, - #[error("From trade balance is too low")] - FromTradeBalanceTooLow, - #[error("Serialization failed")] - SerializeFailed, -} -impl DecodeError for ExchangeError { - fn type_of() -> &'static str { - "ExchangeError" - } -} - -pub struct ExchangeProcessor {} - -impl ExchangeProcessor { - #[allow(clippy::needless_pass_by_value)] - fn map_to_invalid_arg(err: std::boxed::Box) -> InstructionError { - warn!("Deserialize failed, not a valid state: {:?}", err); - InstructionError::InvalidArgument - } - - fn is_account_unallocated(data: &[u8]) -> Result<(), InstructionError> { - let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; - if let ExchangeState::Unallocated = state { - Ok(()) - } else { - error!("New account is already in use"); - Err(InstructionError::InvalidAccountData) - } - } - - fn deserialize_account(data: &[u8]) -> Result { - let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; - if let ExchangeState::Account(account) = state { - Ok(account) - } else { - error!("Not a valid account"); - Err(InstructionError::InvalidAccountData) - } - } - - fn deserialize_order(data: &[u8]) -> Result { - let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; - if let ExchangeState::Trade(info) = state { - Ok(info) - } else { - error!("Not a valid trade"); - Err(InstructionError::InvalidAccountData) - } - } - - fn serialize(state: &ExchangeState, data: &mut [u8]) -> Result<(), InstructionError> { - let writer = std::io::BufWriter::new(data); - match bincode::serialize_into(writer, state) { - Ok(_) => Ok(()), - Err(e) => { - error!("Serialize failed: {:?}", e); - Err(ExchangeError::SerializeFailed.into()) - } - } - } - - fn trade_to_token_account(trade: &OrderInfo) -> TokenAccountInfo { - // Turn trade order into token account - - let token = match trade.side { - OrderSide::Ask => trade.pair.Quote, - OrderSide::Bid => trade.pair.Base, - }; - - let mut account = TokenAccountInfo::default().owner(&trade.owner); - account.tokens[token] = trade.tokens_settled; - account - } - - fn calculate_swap( - scaler: u64, - to_trade: &mut OrderInfo, - from_trade: &mut OrderInfo, - profit_account: &mut TokenAccountInfo, - ) -> Result<(), InstructionError> { - if to_trade.tokens == 0 || from_trade.tokens == 0 { - error!("Inactive Trade, balance is zero"); - return Err(InstructionError::InvalidArgument); - } - if to_trade.price == 0 || from_trade.price == 0 { - error!("Inactive Trade, price is zero"); - return Err(InstructionError::InvalidArgument); - } - - // Calc swap - - trace!("tt {} ft {}", to_trade.tokens, from_trade.tokens); - trace!("tp {} fp {}", to_trade.price, from_trade.price); - - let max_to_secondary = to_trade.tokens * to_trade.price / scaler; - let max_to_primary = from_trade.tokens * scaler / from_trade.price; - - trace!("mtp {} mts {}", max_to_primary, max_to_secondary); - - let max_primary = cmp::min(max_to_primary, to_trade.tokens); - let max_secondary = cmp::min(max_to_secondary, from_trade.tokens); - - trace!("mp {} ms {}", max_primary, max_secondary); - - let primary_tokens = if max_secondary < max_primary { - max_secondary * scaler / from_trade.price - } else { - max_primary - }; - let secondary_tokens = if max_secondary < max_primary { - max_secondary - } else { - max_primary * to_trade.price / scaler - }; - - if primary_tokens == 0 || secondary_tokens == 0 { - error!("Trade quantities to low to be fulfilled"); - return Err(InstructionError::InvalidArgument); - } - - trace!("pt {} st {}", primary_tokens, secondary_tokens); - - let primary_cost = cmp::max(primary_tokens, secondary_tokens * scaler / to_trade.price); - let secondary_cost = cmp::max(secondary_tokens, primary_tokens * from_trade.price / scaler); - - trace!("pc {} sc {}", primary_cost, secondary_cost); - - let primary_profit = primary_cost - primary_tokens; - let secondary_profit = secondary_cost - secondary_tokens; - - trace!("pp {} sp {}", primary_profit, secondary_profit); - - let primary_token = to_trade.pair.Base; - let secondary_token = from_trade.pair.Quote; - - // Update tokens - - if to_trade.tokens < primary_cost { - error!("Not enough tokens in to account"); - return Err(InstructionError::InvalidArgument); - } - if from_trade.tokens < secondary_cost { - error!("Not enough tokens in from account"); - return Err(InstructionError::InvalidArgument); - } - to_trade.tokens -= primary_cost; - to_trade.tokens_settled += secondary_tokens; - from_trade.tokens -= secondary_cost; - from_trade.tokens_settled += primary_tokens; - - profit_account.tokens[primary_token] += primary_profit; - profit_account.tokens[secondary_token] += secondary_profit; - - Ok(()) - } - - fn do_account_request(keyed_accounts: &[KeyedAccount]) -> Result<(), InstructionError> { - const OWNER_INDEX: usize = 0; - const NEW_ACCOUNT_INDEX: usize = 1; - - if keyed_accounts.len() < 2 { - error!("Not enough accounts"); - return Err(InstructionError::InvalidArgument); - } - Self::is_account_unallocated(keyed_accounts[NEW_ACCOUNT_INDEX].try_account_ref()?.data())?; - Self::serialize( - &ExchangeState::Account( - TokenAccountInfo::default() - .owner(keyed_accounts[OWNER_INDEX].unsigned_key()) - .tokens(100_000, 100_000, 100_000, 100_000), - ), - &mut keyed_accounts[NEW_ACCOUNT_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - ) - } - - fn do_transfer_request( - keyed_accounts: &[KeyedAccount], - token: Token, - tokens: u64, - ) -> Result<(), InstructionError> { - const OWNER_INDEX: usize = 0; - const TO_ACCOUNT_INDEX: usize = 1; - const FROM_ACCOUNT_INDEX: usize = 2; - - if keyed_accounts.len() < 3 { - error!("Not enough accounts"); - return Err(InstructionError::InvalidArgument); - } - - let mut to_account = - Self::deserialize_account(keyed_accounts[TO_ACCOUNT_INDEX].try_account_ref()?.data())?; - - if &faucet::id() == keyed_accounts[FROM_ACCOUNT_INDEX].unsigned_key() { - to_account.tokens[token] += tokens; - } else { - let state: ExchangeState = - bincode::deserialize(keyed_accounts[FROM_ACCOUNT_INDEX].try_account_ref()?.data()) - .map_err(Self::map_to_invalid_arg)?; - match state { - ExchangeState::Account(mut from_account) => { - if &from_account.owner != keyed_accounts[OWNER_INDEX].unsigned_key() { - error!("Signer does not own from account"); - return Err(ExchangeError::SignerDoesNotOwnAccount.into()); - } - - if from_account.tokens[token] < tokens { - error!("From account balance too low"); - return Err(ExchangeError::FromAccountBalanceTooLow.into()); - } - - from_account.tokens[token] -= tokens; - to_account.tokens[token] += tokens; - - Self::serialize( - &ExchangeState::Account(from_account), - &mut keyed_accounts[FROM_ACCOUNT_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - )?; - } - ExchangeState::Trade(mut from_trade) => { - if &from_trade.owner != keyed_accounts[OWNER_INDEX].unsigned_key() { - error!("Signer does not own from account"); - return Err(ExchangeError::SignerDoesNotOwnAccount.into()); - } - - let from_token = match from_trade.side { - OrderSide::Ask => from_trade.pair.Quote, - OrderSide::Bid => from_trade.pair.Base, - }; - if token != from_token { - error!("Trade to transfer from does not hold correct token"); - return Err(ExchangeError::TokenMismatch.into()); - } - - if from_trade.tokens_settled < tokens { - error!("From trade balance too low"); - return Err(ExchangeError::FromTradeBalanceTooLow.into()); - } - - from_trade.tokens_settled -= tokens; - to_account.tokens[token] += tokens; - - Self::serialize( - &ExchangeState::Trade(from_trade), - &mut keyed_accounts[FROM_ACCOUNT_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - )?; - } - _ => { - error!("Not a valid from account for transfer"); - return Err(InstructionError::InvalidArgument); - } - } - } - - Self::serialize( - &ExchangeState::Account(to_account), - &mut keyed_accounts[TO_ACCOUNT_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - ) - } - - fn do_order_request( - keyed_accounts: &[KeyedAccount], - info: &OrderRequestInfo, - ) -> Result<(), InstructionError> { - const OWNER_INDEX: usize = 0; - const ORDER_INDEX: usize = 1; - const ACCOUNT_INDEX: usize = 2; - - if keyed_accounts.len() < 3 { - error!("Not enough accounts"); - return Err(InstructionError::InvalidArgument); - } - - Self::is_account_unallocated(keyed_accounts[ORDER_INDEX].try_account_ref()?.data())?; - - let mut account = - Self::deserialize_account(keyed_accounts[ACCOUNT_INDEX].try_account_ref_mut()?.data())?; - - if &account.owner != keyed_accounts[OWNER_INDEX].unsigned_key() { - error!("Signer does not own account"); - return Err(ExchangeError::SignerDoesNotOwnAccount.into()); - } - let from_token = match info.side { - OrderSide::Ask => info.pair.Base, - OrderSide::Bid => info.pair.Quote, - }; - if account.tokens[from_token] < info.tokens { - error!("From token balance is too low"); - return Err(ExchangeError::FromAccountBalanceTooLow.into()); - } - - if let Err(e) = check_trade(info.side, info.tokens, info.price) { - bincode::serialize(&e).unwrap(); - } - - // Trade holds the tokens in escrow - account.tokens[from_token] -= info.tokens; - - inc_new_counter_info!("exchange_processor-trades", 1); - - Self::serialize( - &ExchangeState::Trade(OrderInfo { - owner: *keyed_accounts[OWNER_INDEX].unsigned_key(), - side: info.side, - pair: info.pair, - tokens: info.tokens, - price: info.price, - tokens_settled: 0, - }), - &mut keyed_accounts[ORDER_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - )?; - Self::serialize( - &ExchangeState::Account(account), - &mut keyed_accounts[ACCOUNT_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - ) - } - - fn do_order_cancellation(keyed_accounts: &[KeyedAccount]) -> Result<(), InstructionError> { - const OWNER_INDEX: usize = 0; - const ORDER_INDEX: usize = 1; - - if keyed_accounts.len() < 2 { - error!("Not enough accounts"); - return Err(InstructionError::InvalidArgument); - } - - let order = Self::deserialize_order(keyed_accounts[ORDER_INDEX].try_account_ref()?.data())?; - - if &order.owner != keyed_accounts[OWNER_INDEX].unsigned_key() { - error!("Signer does not own order"); - return Err(ExchangeError::SignerDoesNotOwnOrder.into()); - } - - let token = match order.side { - OrderSide::Ask => order.pair.Base, - OrderSide::Bid => order.pair.Quote, - }; - - let mut account = TokenAccountInfo::default().owner(&order.owner); - account.tokens[token] = order.tokens; - account.tokens[token] += order.tokens_settled; - - // Turn trade order into a token account - Self::serialize( - &ExchangeState::Account(account), - &mut keyed_accounts[ORDER_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - ) - } - - fn do_swap_request(keyed_accounts: &[KeyedAccount]) -> Result<(), InstructionError> { - const TO_ORDER_INDEX: usize = 1; - const FROM_ORDER_INDEX: usize = 2; - const PROFIT_ACCOUNT_INDEX: usize = 3; - - if keyed_accounts.len() < 4 { - error!("Not enough accounts"); - return Err(InstructionError::InvalidArgument); - } - - let mut to_order = - Self::deserialize_order(keyed_accounts[TO_ORDER_INDEX].try_account_ref()?.data())?; - let mut from_order = - Self::deserialize_order(keyed_accounts[FROM_ORDER_INDEX].try_account_ref()?.data())?; - let mut profit_account = Self::deserialize_account( - keyed_accounts[PROFIT_ACCOUNT_INDEX] - .try_account_ref()? - .data(), - )?; - - if to_order.side != OrderSide::Ask { - error!("To trade is not a To"); - return Err(InstructionError::InvalidArgument); - } - if from_order.side != OrderSide::Bid { - error!("From trade is not a From"); - return Err(InstructionError::InvalidArgument); - } - if to_order.pair != from_order.pair { - error!("Mismatched token pairs"); - return Err(InstructionError::InvalidArgument); - } - if to_order.side == from_order.side { - error!("Matching trade sides"); - return Err(InstructionError::InvalidArgument); - } - - if let Err(e) = - Self::calculate_swap(SCALER, &mut to_order, &mut from_order, &mut profit_account) - { - error!( - "Swap calculation failed from {} for {} to {} for {}", - from_order.tokens, from_order.price, to_order.tokens, to_order.price, - ); - return Err(e); - } - - inc_new_counter_info!("exchange_processor-swaps", 1); - - if to_order.tokens == 0 { - // Turn into token account - Self::serialize( - &ExchangeState::Account(Self::trade_to_token_account(&from_order)), - &mut keyed_accounts[TO_ORDER_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - )?; - } else { - Self::serialize( - &ExchangeState::Trade(to_order), - &mut keyed_accounts[TO_ORDER_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - )?; - } - - if from_order.tokens == 0 { - // Turn into token account - Self::serialize( - &ExchangeState::Account(Self::trade_to_token_account(&from_order)), - &mut keyed_accounts[FROM_ORDER_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - )?; - } else { - Self::serialize( - &ExchangeState::Trade(from_order), - &mut keyed_accounts[FROM_ORDER_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - )?; - } - - Self::serialize( - &ExchangeState::Account(profit_account), - &mut keyed_accounts[PROFIT_ACCOUNT_INDEX] - .try_account_ref_mut()? - .data_as_mut_slice(), - ) - } -} - -pub fn process_instruction( - _program_id: &Pubkey, - data: &[u8], - invoke_context: &mut dyn InvokeContext, -) -> Result<(), InstructionError> { - let keyed_accounts = invoke_context.get_keyed_accounts()?; - - solana_logger::setup(); - match limited_deserialize::(data)? { - ExchangeInstruction::AccountRequest => { - ExchangeProcessor::do_account_request(keyed_accounts) - } - ExchangeInstruction::TransferRequest(token, tokens) => { - ExchangeProcessor::do_transfer_request(keyed_accounts, token, tokens) - } - ExchangeInstruction::OrderRequest(info) => { - ExchangeProcessor::do_order_request(keyed_accounts, &info) - } - ExchangeInstruction::OrderCancellation => { - ExchangeProcessor::do_order_cancellation(keyed_accounts) - } - ExchangeInstruction::SwapRequest => ExchangeProcessor::do_swap_request(keyed_accounts), - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{exchange_instruction, id}; - use solana_runtime::bank::Bank; - use solana_runtime::bank_client::BankClient; - use solana_sdk::client::SyncClient; - use solana_sdk::genesis_config::create_genesis_config; - use solana_sdk::message::Message; - use solana_sdk::signature::{Keypair, Signer}; - use solana_sdk::system_instruction; - use std::mem; - - #[allow(clippy::too_many_arguments)] - fn try_calc( - scaler: u64, - primary_tokens: u64, - primary_price: u64, - secondary_tokens: u64, - secondary_price: u64, - primary_tokens_expect: u64, - secondary_tokens_expect: u64, - primary_tokens_settled_expect: u64, - secondary_tokens_settled_expect: u64, - profit_account_tokens: Tokens, - ) -> Result<(), InstructionError> { - trace!( - "Swap {} for {} to {} for {}", - primary_tokens, - primary_price, - secondary_tokens, - secondary_price, - ); - let mut to_trade = OrderInfo::default(); - let mut from_trade = OrderInfo::default().side(OrderSide::Bid); - let mut profit_account = TokenAccountInfo::default(); - - to_trade.tokens = primary_tokens; - to_trade.price = primary_price; - from_trade.tokens = secondary_tokens; - from_trade.price = secondary_price; - ExchangeProcessor::calculate_swap( - scaler, - &mut to_trade, - &mut from_trade, - &mut profit_account, - )?; - - trace!( - "{:?} {:?} {:?} {:?}\n{:?}\n{:?}\n{:?}\n{:?}", - to_trade.tokens, - primary_tokens_expect, - from_trade.tokens, - secondary_tokens_expect, - primary_tokens_settled_expect, - secondary_tokens_settled_expect, - profit_account.tokens, - profit_account_tokens - ); - - assert_eq!(to_trade.tokens, primary_tokens_expect); - assert_eq!(from_trade.tokens, secondary_tokens_expect); - assert_eq!(to_trade.tokens_settled, primary_tokens_settled_expect); - assert_eq!(from_trade.tokens_settled, secondary_tokens_settled_expect); - assert_eq!(profit_account.tokens, profit_account_tokens); - Ok(()) - } - - #[test] - #[rustfmt::skip] - fn test_calculate_swap() { - solana_logger::setup(); - - try_calc(1, 50, 2, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err(); - try_calc(1, 50, 1, 0, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err(); - try_calc(1, 0, 1, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err(); - try_calc(1, 50, 1, 50, 0, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err(); - try_calc(1, 50, 0, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err(); - try_calc(1, 1, 2, 2, 3, 1, 2, 0, 0, Tokens::new( 0, 0, 0, 0)).unwrap_err(); - - try_calc(1, 50, 1, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap(); - try_calc(1, 1, 2, 3, 3, 0, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap(); - try_calc(1, 2, 2, 3, 3, 1, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap(); - try_calc(1, 3, 2, 3, 3, 2, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap(); - try_calc(1, 3, 2, 6, 3, 1, 0, 4, 2, Tokens::new( 0, 2, 0, 0)).unwrap(); - try_calc(1000, 1, 2000, 3, 3000, 0, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap(); - try_calc(1, 3, 2, 7, 3, 1, 1, 4, 2, Tokens::new( 0, 2, 0, 0)).unwrap(); - try_calc(1000, 3000, 333, 1000, 500, 0, 1,999, 1998, Tokens::new(1002, 0, 0, 0)).unwrap(); - try_calc(1000, 50, 100, 50, 101, 0,45, 5, 49, Tokens::new( 1, 0, 0, 0)).unwrap(); - } - - fn create_bank(lamports: u64) -> (Bank, Keypair) { - let (genesis_config, mint_keypair) = create_genesis_config(lamports); - let mut bank = Bank::new(&genesis_config); - bank.add_builtin("exchange_program", id(), process_instruction); - (bank, mint_keypair) - } - - fn create_client(bank: Bank, mint_keypair: Keypair) -> (BankClient, Keypair) { - let owner = Keypair::new(); - let bank_client = BankClient::new(bank); - bank_client - .transfer_and_confirm(42, &mint_keypair, &owner.pubkey()) - .unwrap(); - - (bank_client, owner) - } - - fn create_account(client: &BankClient, owner: &Keypair) -> Pubkey { - let new = Keypair::new(); - - let instruction = system_instruction::create_account( - &owner.pubkey(), - &new.pubkey(), - 1, - mem::size_of::() as u64, - &id(), - ); - - client - .send_and_confirm_message( - &[owner, &new], - Message::new(&[instruction], Some(&owner.pubkey())), - ) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - new.pubkey() - } - - fn create_token_account(client: &BankClient, owner: &Keypair) -> Pubkey { - let new = create_account(client, owner); - let instruction = exchange_instruction::account_request(&owner.pubkey(), &new); - client - .send_and_confirm_instruction(owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - new - } - - fn transfer(client: &BankClient, owner: &Keypair, to: &Pubkey, token: Token, tokens: u64) { - let instruction = exchange_instruction::transfer_request( - &owner.pubkey(), - to, - &faucet::id(), - token, - tokens, - ); - client - .send_and_confirm_instruction(owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - } - - fn trade( - client: &BankClient, - owner: &Keypair, - side: OrderSide, - pair: AssetPair, - from_token: Token, - src_tokens: u64, - trade_tokens: u64, - price: u64, - ) -> (Pubkey, Pubkey) { - let trade = create_account(client, owner); - let src = create_token_account(client, owner); - transfer(client, owner, &src, from_token, src_tokens); - - let instruction = exchange_instruction::trade_request( - &owner.pubkey(), - &trade, - side, - pair, - trade_tokens, - price, - &src, - ); - client - .send_and_confirm_instruction(owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - (trade, src) - } - - #[test] - fn test_exchange_new_account() { - solana_logger::setup(); - let (bank, mint_keypair) = create_bank(10_000); - let (client, owner) = create_client(bank, mint_keypair); - - let new = create_token_account(&client, &owner); - let new_account_data = client.get_account_data(&new).unwrap().unwrap(); - - // Check results - - assert_eq!( - TokenAccountInfo::default() - .owner(&owner.pubkey()) - .tokens(100_000, 100_000, 100_000, 100_000), - ExchangeProcessor::deserialize_account(&new_account_data).unwrap() - ); - } - - #[test] - fn test_exchange_new_account_not_unallocated() { - solana_logger::setup(); - let (bank, mint_keypair) = create_bank(10_000); - let (client, owner) = create_client(bank, mint_keypair); - - let new = create_token_account(&client, &owner); - let instruction = exchange_instruction::account_request(&owner.pubkey(), &new); - client - .send_and_confirm_instruction(&owner, instruction) - .expect_err(&format!("{}:{}", line!(), file!())); - } - - #[test] - fn test_exchange_new_transfer_request() { - solana_logger::setup(); - let (bank, mint_keypair) = create_bank(10_000); - let (client, owner) = create_client(bank, mint_keypair); - - let new = create_token_account(&client, &owner); - - let instruction = exchange_instruction::transfer_request( - &owner.pubkey(), - &new, - &faucet::id(), - Token::A, - 42, - ); - client - .send_and_confirm_instruction(&owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - - let new_account_data = client.get_account_data(&new).unwrap().unwrap(); - - // Check results - - assert_eq!( - TokenAccountInfo::default() - .owner(&owner.pubkey()) - .tokens(100_042, 100_000, 100_000, 100_000), - ExchangeProcessor::deserialize_account(&new_account_data).unwrap() - ); - } - - #[test] - fn test_exchange_new_trade_request() { - solana_logger::setup(); - let (bank, mint_keypair) = create_bank(10_000); - let (client, owner) = create_client(bank, mint_keypair); - - let (trade, src) = trade( - &client, - &owner, - OrderSide::Ask, - AssetPair::default(), - Token::A, - 42, - 2, - 1000, - ); - - let trade_account_data = client.get_account_data(&trade).unwrap().unwrap(); - let src_account_data = client.get_account_data(&src).unwrap().unwrap(); - - // check results - - assert_eq!( - OrderInfo { - owner: owner.pubkey(), - side: OrderSide::Ask, - pair: AssetPair::default(), - tokens: 2, - price: 1000, - tokens_settled: 0 - }, - ExchangeProcessor::deserialize_order(&trade_account_data).unwrap() - ); - assert_eq!( - TokenAccountInfo::default() - .owner(&owner.pubkey()) - .tokens(100_040, 100_000, 100_000, 100_000), - ExchangeProcessor::deserialize_account(&src_account_data).unwrap() - ); - } - - #[test] - fn test_exchange_new_swap_request() { - solana_logger::setup(); - let (bank, mint_keypair) = create_bank(10_000); - let (client, owner) = create_client(bank, mint_keypair); - - let profit = create_token_account(&client, &owner); - let (to_trade, _) = trade( - &client, - &owner, - OrderSide::Ask, - AssetPair::default(), - Token::A, - 2, - 2, - 2000, - ); - let (from_trade, _) = trade( - &client, - &owner, - OrderSide::Bid, - AssetPair::default(), - Token::B, - 3, - 3, - 3000, - ); - - let instruction = - exchange_instruction::swap_request(&owner.pubkey(), &to_trade, &from_trade, &profit); - client - .send_and_confirm_instruction(&owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - - let to_trade_account_data = client.get_account_data(&to_trade).unwrap().unwrap(); - let from_trade_account_data = client.get_account_data(&from_trade).unwrap().unwrap(); - let profit_account_data = client.get_account_data(&profit).unwrap().unwrap(); - - // check results - - assert_eq!( - OrderInfo { - owner: owner.pubkey(), - side: OrderSide::Ask, - pair: AssetPair::default(), - tokens: 1, - price: 2000, - tokens_settled: 2, - }, - ExchangeProcessor::deserialize_order(&to_trade_account_data).unwrap() - ); - - assert_eq!( - TokenAccountInfo::default() - .owner(&owner.pubkey()) - .tokens(1, 0, 0, 0), - ExchangeProcessor::deserialize_account(&from_trade_account_data).unwrap() - ); - - assert_eq!( - TokenAccountInfo::default() - .owner(&owner.pubkey()) - .tokens(100_000, 100_001, 100_000, 100_000), - ExchangeProcessor::deserialize_account(&profit_account_data).unwrap() - ); - } - - #[test] - fn test_exchange_trade_to_token_account() { - solana_logger::setup(); - let (bank, mint_keypair) = create_bank(10_000); - let (client, owner) = create_client(bank, mint_keypair); - - let profit = create_token_account(&client, &owner); - let (to_trade, _) = trade( - &client, - &owner, - OrderSide::Ask, - AssetPair::default(), - Token::A, - 3, - 3, - 2000, - ); - let (from_trade, _) = trade( - &client, - &owner, - OrderSide::Bid, - AssetPair::default(), - Token::B, - 3, - 3, - 3000, - ); - - let instruction = - exchange_instruction::swap_request(&owner.pubkey(), &to_trade, &from_trade, &profit); - client - .send_and_confirm_instruction(&owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - - let new = create_token_account(&client, &owner); - - let instruction = - exchange_instruction::transfer_request(&owner.pubkey(), &new, &to_trade, Token::B, 1); - client - .send_and_confirm_instruction(&owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - - let instruction = - exchange_instruction::transfer_request(&owner.pubkey(), &new, &from_trade, Token::A, 1); - client - .send_and_confirm_instruction(&owner, instruction) - .unwrap_or_else(|_| panic!("{}:{}", line!(), file!())); - - let new_account_data = client.get_account_data(&new).unwrap().unwrap(); - - // Check results - - assert_eq!( - TokenAccountInfo::default() - .owner(&owner.pubkey()) - .tokens(100_001, 100_001, 100_000, 100_000), - ExchangeProcessor::deserialize_account(&new_account_data).unwrap() - ); - } -} diff --git a/programs/exchange/src/exchange_state.rs b/programs/exchange/src/exchange_state.rs deleted file mode 100644 index cd3101cf1..000000000 --- a/programs/exchange/src/exchange_state.rs +++ /dev/null @@ -1,226 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; -use std::{error, fmt}; - -/// Fixed-point scaler, 10 = one base 10 digit to the right of the decimal, 100 = 2, ... -/// Used by both price and amount in their fixed point representation -pub const SCALER: u64 = 1000; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum ExchangeError { - InvalidTrade(String), -} -impl error::Error for ExchangeError {} -impl fmt::Display for ExchangeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ExchangeError::InvalidTrade(s) => write!(f, "{}", s), - } - } -} - -/// Supported token types -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum Token { - A, - B, - C, - D, -} -impl Default for Token { - fn default() -> Self { - Token::A - } -} - -// Values of tokens, could be quantities, prices, etc... -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -#[allow(non_snake_case)] -pub struct Tokens { - pub A: u64, - pub B: u64, - pub C: u64, - pub D: u64, -} -impl Tokens { - pub fn new(a: u64, b: u64, c: u64, d: u64) -> Self { - Self { - A: a, - B: b, - C: c, - D: d, - } - } -} -impl std::ops::Index for Tokens { - type Output = u64; - fn index(&self, t: Token) -> &u64 { - match t { - Token::A => &self.A, - Token::B => &self.B, - Token::C => &self.C, - Token::D => &self.D, - } - } -} -impl std::ops::IndexMut for Tokens { - fn index_mut(&mut self, t: Token) -> &mut u64 { - match t { - Token::A => &mut self.A, - Token::B => &mut self.B, - Token::C => &mut self.C, - Token::D => &mut self.D, - } - } -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[allow(non_snake_case)] -pub struct AssetPair { - // represents a pair of two token enums that defines a market - pub Base: Token, - // "primary" token and numerator for pricing purposes - pub Quote: Token, - // "secondary" token and denominator for pricing purposes -} - -impl Default for AssetPair { - fn default() -> AssetPair { - AssetPair { - Base: Token::A, - Quote: Token::B, - } - } -} - -/// Token accounts are populated with this structure -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct TokenAccountInfo { - /// Investor who owns this account - pub owner: Pubkey, - /// Current number of tokens this account holds - pub tokens: Tokens, -} - -impl TokenAccountInfo { - pub fn owner(mut self, owner: &Pubkey) -> Self { - self.owner = *owner; - self - } - pub fn tokens(mut self, a: u64, b: u64, c: u64, d: u64) -> Self { - self.tokens = Tokens { - A: a, - B: b, - C: c, - D: d, - }; - self - } -} - -/// side of the exchange between two tokens in a pair -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum OrderSide { - /// Offer the Base asset and Accept the Quote asset - Ask, // to - /// Offer the Quote asset and Accept the Base asset - Bid, // from -} -impl fmt::Display for OrderSide { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - OrderSide::Ask => write!(f, "A")?, - OrderSide::Bid => write!(f, "B")?, - } - Ok(()) - } -} - -/// Trade accounts are populated with this structure -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct OrderInfo { - /// Owner of the trade order - pub owner: Pubkey, - /// side of the order in the market (bid/ask) - pub side: OrderSide, - /// Token pair indicating two tokens to exchange, first is primary - pub pair: AssetPair, - /// Number of tokens to exchange; primary or secondary depending on side. Once - /// this number goes to zero this trade order will be converted into a regular token account - pub tokens: u64, - /// Scaled price of the secondary token given the primary is equal to the scale value - /// If scale is 1 and price is 2 then ratio is 1:2 or 1 primary token for 2 secondary tokens - pub price: u64, - /// Number of tokens that have been settled so far. These nay be transferred to another - /// token account by the owner. - pub tokens_settled: u64, -} -impl Default for OrderInfo { - fn default() -> Self { - Self { - owner: Pubkey::default(), - pair: AssetPair::default(), - side: OrderSide::Ask, - tokens: 0, - price: 0, - tokens_settled: 0, - } - } -} -impl OrderInfo { - pub fn pair(mut self, pair: AssetPair) -> Self { - self.pair = pair; - self - } - pub fn side(mut self, side: OrderSide) -> Self { - self.side = side; - self - } - pub fn tokens(mut self, tokens: u64) -> Self { - self.tokens = tokens; - self - } - pub fn price(mut self, price: u64) -> Self { - self.price = price; - self - } -} - -pub fn check_trade(side: OrderSide, tokens: u64, price: u64) -> Result<(), ExchangeError> { - match side { - OrderSide::Ask => { - if tokens * price / SCALER == 0 { - return Err(ExchangeError::InvalidTrade(format!( - "To trade of {} for {}/{} results in 0 tradeable tokens", - tokens, SCALER, price - ))); - } - } - OrderSide::Bid => { - if tokens * SCALER / price == 0 { - return Err(ExchangeError::InvalidTrade(format!( - "From trade of {} for {}?{} results in 0 tradeable tokens", - tokens, SCALER, price - ))); - } - } - } - Ok(()) -} - -/// Type of exchange account, account's user data is populated with this enum -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum ExchangeState { - /// Account's data is unallocated - Unallocated, - // Token account - Account(TokenAccountInfo), - // Trade order account - Trade(OrderInfo), - Invalid, -} -impl Default for ExchangeState { - fn default() -> Self { - ExchangeState::Unallocated - } -} diff --git a/programs/exchange/src/lib.rs b/programs/exchange/src/lib.rs deleted file mode 100644 index 21a0cb356..000000000 --- a/programs/exchange/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -#![allow(clippy::integer_arithmetic)] -pub mod exchange_instruction; -pub mod exchange_processor; -pub mod exchange_state; - -#[macro_use] -extern crate solana_metrics; - -use crate::exchange_processor::process_instruction; - -solana_sdk::declare_program!( - "Exchange11111111111111111111111111111111111", - solana_exchange_program, - process_instruction -); - -pub mod faucet { - solana_sdk::declare_id!("ExchangeFaucet11111111111111111111111111111"); -} diff --git a/scripts/cargo-install-all.sh b/scripts/cargo-install-all.sh index 3fad4c41c..2889c4d4d 100755 --- a/scripts/cargo-install-all.sh +++ b/scripts/cargo-install-all.sh @@ -84,7 +84,6 @@ else BINS=( solana - solana-bench-exchange solana-bench-tps solana-faucet solana-gossip diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 3b241041b..aedae028c 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -106,7 +106,6 @@ find target/cov -type f -name '*.gcda' -newer target/cov/before-test ! -newer ta --ignore bench-tps\* --ignore upload-perf\* --ignore bench-streamer\* - --ignore bench-exchange\* --ignore local-cluster\* )