diff --git a/.gitignore b/.gitignore index de2f9e697b558c..2dbde8e1bca992 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ /book/html/ /book/src/img/ /book/src/tests.ok -/proposals/html/ -/proposals/src/img/ -/proposals/src/tests.ok **/*.rs.bk .cargo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8095b892b3023..e3daae2cefcb62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,13 +92,16 @@ understood. Avoid introducing new 3-letter terms, which can be confused with 3-l Design Proposals --- -Solana's design proposals are part of a book generated from markdown files in -the `proposals/src/` directory. To add a design proposal: +Solana's architecture is described by a book generated from markdown files in +the `book/src/` directory, maintained by an *editor* (currently @garious). To +add a design proposal, you'll need to at least propose a change the content +under the [Accepted Design +Proposals](https://solana-labs.github.io/book-edge/proposals.html) chapter. +Here's the full process: 1. Propose a design by creating a PR that adds a markdown document to the - directory `proposals/src/` and references it from the [table of - contents](proposals/src/SUMMARY.md). Add any relevant *maintainers* to the PR - review. + directory `book/src/` and references it from the [table of + contents](book/src/SUMMARY.md). Add any relevant *maintainers* to the PR review. 2. The PR being merged indicates your proposed change was accepted and that the maintainers support your plan of attack. 3. Submit PRs that implement the proposal. When the implementation reveals the diff --git a/Cargo.lock b/Cargo.lock index cfda020b9d5194..bcb1fd0de3dafa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "assert_cmd" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "escargot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2234,6 +2234,29 @@ dependencies = [ "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "solana-exchange-api" +version = "0.13.0" +dependencies = [ + "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-logger 0.13.0", + "solana-runtime 0.13.0", + "solana-sdk 0.13.0", +] + +[[package]] +name = "solana-exchange-program" +version = "0.13.0" +dependencies = [ + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-exchange-api 0.13.0", + "solana-logger 0.13.0", + "solana-sdk 0.13.0", +] + [[package]] name = "solana-failure" version = "0.13.0" @@ -2271,6 +2294,7 @@ dependencies = [ "solana 0.13.0", "solana-budget-api 0.13.0", "solana-config-api 0.13.0", + "solana-exchange-api 0.13.0", "solana-sdk 0.13.0", "solana-storage-api 0.13.0", "solana-token-api 0.13.0", @@ -2333,7 +2357,7 @@ dependencies = [ name = "solana-ledger-tool" version = "0.13.0" dependencies = [ - "assert_cmd 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "assert_cmd 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "solana 0.13.0", @@ -3131,7 +3155,7 @@ dependencies = [ "checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" "checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71" "checksum ascii 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae7d751998c189c1d4468cf0a39bb2eae052a9c58d50ebb3b9591ee3813ad50" -"checksum assert_cmd 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7eaef71d143e8053e28166ea984712b71a5e5d7f26a885809eb829e0c8e4f051" +"checksum assert_cmd 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2dc477793bd82ec39799b6f6b3df64938532fdf2ab0d49ef817eac65856a5a1e" "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" "checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" "checksum backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5a90e2b463010cd0e0ce9a11d4a9d5d58d9f41d4a6ba3dcaf9e68b466e88b4" diff --git a/Cargo.toml b/Cargo.toml index 3a54ab582dacdb..c98c0a6c34d562 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ members = [ "programs/budget_program", "programs/config_api", "programs/config_program", + "programs/exchange_api", + "programs/exchange_program", "programs/token_api", "programs/token_program", "programs/failure_program", diff --git a/README.md b/README.md index 491e52ca4f699e..c6ecda4584d5b6 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,6 @@ Before you jump into the code, review the online book [Solana: Blockchain Rebuil (The _latest_ development version of the online book is also [available here](https://solana-labs.github.io/book-edge/).) -Design proposals that have not yet been integrated into the online book can be -viewed [here](https://solana-labs.github.io/book-edge/proposals/index.html). - Developing === diff --git a/book/README.md b/book/README.md index 51fd9681bf7a78..f7aa8bb7624555 100644 --- a/book/README.md +++ b/book/README.md @@ -1,7 +1,7 @@ Building the Solana book --- -Install the book's dependencies, build, and test the book: +Install the book's dependnecies, build, and test the book: ```bash $ ./build.sh diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 7409ba604c165f..29588ca7aee33e 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -34,3 +34,30 @@ - [JavaScript API](javascript-api.md) - [solana-wallet CLI](wallet.md) +- [Accepted Design Proposals](proposals.md) + - [Ledger Replication](ledger-replication-to-implement.md) + - [Secure Vote Signing](vote-signing-to-implement.md) + - [Staking Rewards](staking-rewards.md) + - [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md) + - [Reliable Vote Transmission](reliable-vote-transmission.md) + - [Persistent Account Storage](persistent-account-storage.md) + - [Cluster Economics](ed_overview.md) + - [Validation-client Economics](ed_validation_client_economics.md) + - [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md) + - [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) + - [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md) + - [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) + - [Replication-client Economics](ed_replication_client_economics.md) + - [Storage-replication Rewards](ed_rce_storage_replication_rewards.md) + - [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md) + - [Economic Sustainability](ed_economic_sustainability.md) + - [Attack Vectors](ed_attack_vectors.md) + - [Economic Design MVP](ed_mvp.md) + - [References](ed_references.md) + - [Cluster Test Framework](cluster-test-framework.md) + - [Testing Programs](testing-programs.md) + +- [Implemented Design Proposals](implemented-proposals.md) + - [Leader-to-Leader Transition](leader-leader-transition.md) + - [Leader-to-Validator Transition](leader-validator-transition.md) + - [Fork Selection](fork-selection.md) diff --git a/proposals/src/cluster-test-framework.md b/book/src/cluster-test-framework.md similarity index 100% rename from proposals/src/cluster-test-framework.md rename to book/src/cluster-test-framework.md diff --git a/proposals/src/ed_attack_vectors.md b/book/src/ed_attack_vectors.md similarity index 100% rename from proposals/src/ed_attack_vectors.md rename to book/src/ed_attack_vectors.md diff --git a/proposals/src/ed_economic_sustainability.md b/book/src/ed_economic_sustainability.md similarity index 100% rename from proposals/src/ed_economic_sustainability.md rename to book/src/ed_economic_sustainability.md diff --git a/proposals/src/ed_mvp.md b/book/src/ed_mvp.md similarity index 100% rename from proposals/src/ed_mvp.md rename to book/src/ed_mvp.md diff --git a/proposals/src/ed_overview.md b/book/src/ed_overview.md similarity index 100% rename from proposals/src/ed_overview.md rename to book/src/ed_overview.md diff --git a/proposals/src/ed_rce_replication_client_reward_auto_delegation.md b/book/src/ed_rce_replication_client_reward_auto_delegation.md similarity index 100% rename from proposals/src/ed_rce_replication_client_reward_auto_delegation.md rename to book/src/ed_rce_replication_client_reward_auto_delegation.md diff --git a/proposals/src/ed_rce_storage_replication_rewards.md b/book/src/ed_rce_storage_replication_rewards.md similarity index 100% rename from proposals/src/ed_rce_storage_replication_rewards.md rename to book/src/ed_rce_storage_replication_rewards.md diff --git a/proposals/src/ed_references.md b/book/src/ed_references.md similarity index 100% rename from proposals/src/ed_references.md rename to book/src/ed_references.md diff --git a/proposals/src/ed_replication_client_economics.md b/book/src/ed_replication_client_economics.md similarity index 100% rename from proposals/src/ed_replication_client_economics.md rename to book/src/ed_replication_client_economics.md diff --git a/proposals/src/ed_validation_client_economics.md b/book/src/ed_validation_client_economics.md similarity index 100% rename from proposals/src/ed_validation_client_economics.md rename to book/src/ed_validation_client_economics.md diff --git a/proposals/src/ed_vce_replication_validation_transaction_fees.md b/book/src/ed_vce_replication_validation_transaction_fees.md similarity index 100% rename from proposals/src/ed_vce_replication_validation_transaction_fees.md rename to book/src/ed_vce_replication_validation_transaction_fees.md diff --git a/proposals/src/ed_vce_state_validation_protocol_based_rewards.md b/book/src/ed_vce_state_validation_protocol_based_rewards.md similarity index 100% rename from proposals/src/ed_vce_state_validation_protocol_based_rewards.md rename to book/src/ed_vce_state_validation_protocol_based_rewards.md diff --git a/proposals/src/ed_vce_state_validation_transaction_fees.md b/book/src/ed_vce_state_validation_transaction_fees.md similarity index 100% rename from proposals/src/ed_vce_state_validation_transaction_fees.md rename to book/src/ed_vce_state_validation_transaction_fees.md diff --git a/proposals/src/ed_vce_validation_stake_delegation.md b/book/src/ed_vce_validation_stake_delegation.md similarity index 100% rename from proposals/src/ed_vce_validation_stake_delegation.md rename to book/src/ed_vce_validation_stake_delegation.md diff --git a/proposals/src/fork-selection.md b/book/src/fork-selection.md similarity index 100% rename from proposals/src/fork-selection.md rename to book/src/fork-selection.md diff --git a/proposals/src/img/porep_reward.png b/book/src/img/porep_reward.png similarity index 100% rename from proposals/src/img/porep_reward.png rename to book/src/img/porep_reward.png diff --git a/proposals/src/img/solana_economic_design.png b/book/src/img/solana_economic_design.png similarity index 100% rename from proposals/src/img/solana_economic_design.png rename to book/src/img/solana_economic_design.png diff --git a/proposals/src/img/validation_client_interest_rates.png b/book/src/img/validation_client_interest_rates.png similarity index 100% rename from proposals/src/img/validation_client_interest_rates.png rename to book/src/img/validation_client_interest_rates.png diff --git a/proposals/src/implemented-proposals.md b/book/src/implemented-proposals.md similarity index 100% rename from proposals/src/implemented-proposals.md rename to book/src/implemented-proposals.md diff --git a/proposals/src/leader-leader-transition.md b/book/src/leader-leader-transition.md similarity index 100% rename from proposals/src/leader-leader-transition.md rename to book/src/leader-leader-transition.md diff --git a/proposals/src/leader-validator-transition.md b/book/src/leader-validator-transition.md similarity index 100% rename from proposals/src/leader-validator-transition.md rename to book/src/leader-validator-transition.md diff --git a/proposals/src/ledger-replication-to-implement.md b/book/src/ledger-replication-to-implement.md similarity index 100% rename from proposals/src/ledger-replication-to-implement.md rename to book/src/ledger-replication-to-implement.md diff --git a/proposals/src/passive-stake-delegation-and-rewards.md b/book/src/passive-stake-delegation-and-rewards.md similarity index 100% rename from proposals/src/passive-stake-delegation-and-rewards.md rename to book/src/passive-stake-delegation-and-rewards.md diff --git a/proposals/src/persistent-account-storage.md b/book/src/persistent-account-storage.md similarity index 100% rename from proposals/src/persistent-account-storage.md rename to book/src/persistent-account-storage.md diff --git a/book/src/proposals.md b/book/src/proposals.md new file mode 100644 index 00000000000000..a5b884e84fc99a --- /dev/null +++ b/book/src/proposals.md @@ -0,0 +1,7 @@ +# Proposed Architectural Changes + +The following architectural proposals have been accepted by the Solana team, but +are not yet fully implemented. The proposals may be implemented as described, +implemented differently as issues in the designs become evident, or not +implemented at all. If implemented, the descriptions will be moved from this +section to earlier chapters in a future version of this book. diff --git a/proposals/src/reliable-vote-transmission.md b/book/src/reliable-vote-transmission.md similarity index 100% rename from proposals/src/reliable-vote-transmission.md rename to book/src/reliable-vote-transmission.md diff --git a/proposals/src/staking-rewards.md b/book/src/staking-rewards.md similarity index 100% rename from proposals/src/staking-rewards.md rename to book/src/staking-rewards.md diff --git a/proposals/src/testing-programs.md b/book/src/testing-programs.md similarity index 100% rename from proposals/src/testing-programs.md rename to book/src/testing-programs.md diff --git a/proposals/src/vote-signing-to-implement.md b/book/src/vote-signing-to-implement.md similarity index 100% rename from proposals/src/vote-signing-to-implement.md rename to book/src/vote-signing-to-implement.md diff --git a/ci/publish-book.sh b/ci/publish-book.sh index b8c43a747b634c..301035bd624e6b 100755 --- a/ci/publish-book.sh +++ b/ci/publish-book.sh @@ -4,15 +4,10 @@ set -e cd "$(dirname "$0")/.." book/build.sh -proposals/build.sh echo --- create book repo ( set -x - - test ! -d book/html/proposals - mv proposals/html book/html/proposals/ - cd book/html/ git init . git config user.email "maintainers@solana.com" diff --git a/ci/test-checks.sh b/ci/test-checks.sh index f21f7346057101..301c120c622012 100755 --- a/ci/test-checks.sh +++ b/ci/test-checks.sh @@ -15,6 +15,5 @@ _ cargo +"$rust_stable" clippy --all -- --deny=warnings _ ci/audit.sh _ ci/nits.sh _ book/build.sh -_ proposals/build.sh echo --- ok diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 4fb9ab3ce50e86..3521030a6b55b7 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -249,24 +249,22 @@ impl BankingStage { Ok(()) } - pub fn process_and_record_transactions( + fn process_and_record_transactions_locked( bank: &Bank, txs: &[Transaction], poh: &Arc>, + lock_results: &[bank::Result<()>], ) -> Result<()> { - let now = Instant::now(); - // Once accounts are locked, other threads cannot encode transactions that will modify the - // same account state - let lock_results = bank.lock_accounts(txs); - let lock_time = now.elapsed(); - let now = Instant::now(); // Use a shorter maximum age when adding transactions into the pipeline. This will reduce // the likelihood of any single thread getting starved and processing old ids. // TODO: Banking stage threads should be prioritized to complete faster then this queue // expires. - let (loaded_accounts, results) = - bank.load_and_execute_transactions(txs, lock_results, MAX_RECENT_BLOCKHASHES / 2); + let (loaded_accounts, results) = bank.load_and_execute_transactions( + txs, + lock_results.to_vec(), + MAX_RECENT_BLOCKHASHES / 2, + ); let load_execute_time = now.elapsed(); let record_time = { @@ -281,21 +279,45 @@ impl BankingStage { now.elapsed() }; + debug!( + "bank: {} load_execute: {}us record: {}us commit: {}us txs_len: {}", + bank.slot(), + duration_as_us(&load_execute_time), + duration_as_us(&record_time), + duration_as_us(&commit_time), + txs.len(), + ); + + Ok(()) + } + + pub fn process_and_record_transactions( + bank: &Bank, + txs: &[Transaction], + poh: &Arc>, + ) -> Result<()> { + let now = Instant::now(); + // Once accounts are locked, other threads cannot encode transactions that will modify the + // same account state + let lock_results = bank.lock_accounts(txs); + let lock_time = now.elapsed(); + + let results = Self::process_and_record_transactions_locked(bank, txs, poh, &lock_results); + let now = Instant::now(); // Once the accounts are new transactions can enter the pipeline to process them - bank.unlock_accounts(&txs, &results); + bank.unlock_accounts(&txs, &lock_results); let unlock_time = now.elapsed(); + debug!( - "bank: {} lock: {}us load_execute: {}us record: {}us commit: {}us unlock: {}us txs_len: {}", + "bank: {} lock: {}us unlock: {}us txs_len: {}", bank.slot(), duration_as_us(&lock_time), - duration_as_us(&load_execute_time), - duration_as_us(&record_time), - duration_as_us(&commit_time), duration_as_us(&unlock_time), txs.len(), ); - Ok(()) + + results } /// Sends transactions to the bank. @@ -481,9 +503,9 @@ mod tests { use crate::packet::to_packets; use crate::poh_recorder::WorkingBank; use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::instruction::InstructionError; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::system_transaction::SystemTransaction; - use solana_sdk::transaction::InstructionError; use std::sync::mpsc::channel; use std::thread::sleep; diff --git a/core/src/cluster.rs b/core/src/cluster.rs new file mode 100644 index 00000000000000..7142792355924a --- /dev/null +++ b/core/src/cluster.rs @@ -0,0 +1,6 @@ +use solana_sdk::pubkey::Pubkey; + +pub trait Cluster { + fn get_node_ids(&self) -> Vec; + fn restart_node(&mut self, pubkey: Pubkey); +} diff --git a/core/src/cluster_tests.rs b/core/src/cluster_tests.rs index e9153c675c8b9a..577bc563eb3657 100644 --- a/core/src/cluster_tests.rs +++ b/core/src/cluster_tests.rs @@ -8,12 +8,13 @@ use crate::contact_info::ContactInfo; use crate::entry::{Entry, EntrySlice}; use crate::gossip_service::discover; use crate::locktower::VOTE_THRESHOLD_DEPTH; +use crate::poh_service::PohServiceConfig; use solana_client::thin_client::create_client; use solana_sdk::hash::Hash; use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; use solana_sdk::system_transaction::SystemTransaction; use solana_sdk::timing::{ - DEFAULT_TICKS_PER_SLOT, NUM_CONSECUTIVE_LEADER_SLOTS, NUM_TICKS_PER_SECOND, + duration_as_ms, DEFAULT_TICKS_PER_SLOT, NUM_CONSECUTIVE_LEADER_SLOTS, NUM_TICKS_PER_SECOND, }; use std::io; use std::thread::sleep; @@ -122,6 +123,25 @@ pub fn verify_ledger_ticks(ledger_path: &str, ticks_per_slot: usize) { } } +pub fn sleep_n_epochs( + num_epochs: f64, + config: &PohServiceConfig, + ticks_per_slot: u64, + slots_per_epoch: u64, +) { + let num_ticks_per_second = { + match config { + PohServiceConfig::Sleep(d) => (1000 / duration_as_ms(d)) as f64, + _ => panic!("Unsuppported tick config for testing"), + } + }; + + let num_ticks_to_sleep = num_epochs * ticks_per_slot as f64 * slots_per_epoch as f64; + sleep(Duration::from_secs( + ((num_ticks_to_sleep + num_ticks_per_second - 1.0) / num_ticks_per_second) as u64, + )); +} + pub fn kill_entry_and_spend_and_verify_rest( entry_point_info: &ContactInfo, funding_keypair: &Keypair, diff --git a/core/src/fullnode.rs b/core/src/fullnode.rs index 785777b648666f..df11b1ed138f87 100644 --- a/core/src/fullnode.rs +++ b/core/src/fullnode.rs @@ -34,6 +34,7 @@ use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex, RwLock}; use std::thread::Result; +#[derive(Clone)] pub struct FullnodeConfig { pub sigverify_disabled: bool, pub voting_disabled: bool, @@ -248,15 +249,6 @@ impl Fullnode { // Used for notifying many nodes in parallel to exit pub fn exit(&self) { self.exit.store(true, Ordering::Relaxed); - - // Need to force the poh_recorder to drop the WorkingBank, - // which contains the channel to BroadcastStage. This should be - // sufficient as long as no other rotations are happening that - // can cause the Tpu to restart a BankingStage and reset a - // WorkingBank in poh_recorder. It follows no other rotations can be - // in motion because exit()/close() are only called by the run() loop - // which is the sole initiator of rotations. - self.poh_recorder.lock().unwrap().clear_bank(); } pub fn close(self) -> Result<()> { diff --git a/core/src/lib.rs b/core/src/lib.rs index e3b732522422ac..96d0b6d6797c02 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -27,6 +27,7 @@ pub mod blocktree; pub mod blockstream; pub mod blockstream_service; pub mod blocktree_processor; +pub mod cluster; pub mod cluster_info; pub mod cluster_tests; pub mod db_window; diff --git a/core/src/local_cluster.rs b/core/src/local_cluster.rs index ecb4f2a04099c0..49a036183f23e8 100644 --- a/core/src/local_cluster.rs +++ b/core/src/local_cluster.rs @@ -1,4 +1,5 @@ use crate::blocktree::{create_new_tmp_ledger, tmp_copy_blocktree}; +use crate::cluster::Cluster; use crate::cluster_info::{Node, FULLNODE_PORT_RANGE}; use crate::contact_info::ContactInfo; use crate::fullnode::{Fullnode, FullnodeConfig}; @@ -15,20 +16,37 @@ use solana_sdk::timing::DEFAULT_SLOTS_PER_EPOCH; use solana_sdk::timing::DEFAULT_TICKS_PER_SLOT; use solana_vote_api::vote_state::VoteState; use solana_vote_api::vote_transaction::VoteTransaction; +use std::collections::HashMap; use std::fs::remove_dir_all; use std::io::{Error, ErrorKind, Result}; use std::sync::Arc; +pub struct FullnodeInfo { + pub keypair: Arc, + pub ledger_path: String, +} + +impl FullnodeInfo { + fn new(keypair: Arc, ledger_path: String) -> Self { + Self { + keypair, + ledger_path, + } + } +} + pub struct LocalCluster { /// Keypair with funding to particpiate in the network pub funding_keypair: Keypair, + pub fullnode_config: FullnodeConfig, /// Entry point from which the rest of the network can be discovered pub entry_point_info: ContactInfo, - pub ledger_paths: Vec, - fullnodes: Vec, - replicators: Vec, + pub fullnode_infos: HashMap, + fullnodes: HashMap, genesis_ledger_path: String, genesis_block: GenesisBlock, + replicators: Vec, + pub replicator_ledger_paths: Vec, } impl LocalCluster { @@ -86,9 +104,6 @@ impl LocalCluster { genesis_block.slots_per_epoch = slots_per_epoch; let (genesis_ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block); let leader_ledger_path = tmp_copy_blocktree!(&genesis_ledger_path); - let mut ledger_paths = vec![]; - ledger_paths.push(genesis_ledger_path.clone()); - ledger_paths.push(leader_ledger_path.clone()); let voting_keypair = Keypair::new(); let leader_contact_info = leader_node.info.clone(); @@ -102,16 +117,24 @@ impl LocalCluster { fullnode_config, ); - let fullnodes = vec![leader_server]; + let mut fullnodes = HashMap::new(); + let mut fullnode_infos = HashMap::new(); + fullnodes.insert(leader_pubkey, leader_server); + fullnode_infos.insert( + leader_pubkey, + FullnodeInfo::new(leader_keypair.clone(), leader_ledger_path), + ); let mut cluster = Self { funding_keypair: mint_keypair, entry_point_info: leader_contact_info, fullnodes, replicators: vec![], - ledger_paths, + replicator_ledger_paths: vec![], genesis_ledger_path, genesis_block, + fullnode_infos, + fullnode_config: fullnode_config.clone(), }; for stake in &node_stakes[1..] { @@ -128,14 +151,14 @@ impl LocalCluster { } pub fn exit(&self) { - for node in &self.fullnodes { + for node in self.fullnodes.values() { node.exit(); } } pub fn close_preserve_ledgers(&mut self) { self.exit(); - while let Some(node) = self.fullnodes.pop() { + for (_, node) in self.fullnodes.drain() { node.join().unwrap(); } @@ -157,7 +180,6 @@ impl LocalCluster { let validator_pubkey = validator_keypair.pubkey(); let validator_node = Node::new_localhost_with_pubkey(&validator_keypair.pubkey()); let ledger_path = tmp_copy_blocktree!(&self.genesis_ledger_path); - self.ledger_paths.push(ledger_path.clone()); // Send each validator some lamports to vote let validator_balance = @@ -180,7 +202,12 @@ impl LocalCluster { fullnode_config, ); - self.fullnodes.push(validator_server); + self.fullnodes + .insert(validator_keypair.pubkey(), validator_server); + self.fullnode_infos.insert( + validator_keypair.pubkey(), + FullnodeInfo::new(validator_keypair.clone(), ledger_path), + ); } fn add_replicator(&mut self) { @@ -208,15 +235,20 @@ impl LocalCluster { ) .unwrap(); - self.ledger_paths.push(replicator_ledger_path); - + self.replicator_ledger_paths.push(replicator_ledger_path); self.replicators.push(replicator); } fn close(&mut self) { self.close_preserve_ledgers(); - for path in &self.ledger_paths { - remove_dir_all(path).unwrap_or_else(|_| panic!("Unable to remove {}", path)); + for ledger_path in self + .fullnode_infos + .values() + .map(|f| &f.ledger_path) + .chain(self.replicator_ledger_paths.iter()) + { + remove_dir_all(&ledger_path) + .unwrap_or_else(|_| panic!("Unable to remove {}", ledger_path)); } } @@ -300,6 +332,38 @@ impl LocalCluster { } } +impl Cluster for LocalCluster { + fn restart_node(&mut self, pubkey: Pubkey) { + // Shut down the fullnode + let node = self.fullnodes.remove(&pubkey).unwrap(); + node.exit(); + node.join().unwrap(); + + // Restart the node + let fullnode_info = &self.fullnode_infos[&pubkey]; + let node = Node::new_localhost_with_pubkey(&fullnode_info.keypair.pubkey()); + if pubkey == self.entry_point_info.id { + self.entry_point_info = node.info.clone(); + } + let new_voting_keypair = Keypair::new(); + let restarted_node = Fullnode::new( + node, + &fullnode_info.keypair, + &fullnode_info.ledger_path, + &new_voting_keypair.pubkey(), + new_voting_keypair, + None, + &self.fullnode_config, + ); + + self.fullnodes.insert(pubkey, restarted_node); + } + + fn get_node_ids(&self) -> Vec { + self.fullnodes.keys().cloned().collect() + } +} + impl Drop for LocalCluster { fn drop(&mut self) { self.close(); diff --git a/core/src/poh_recorder.rs b/core/src/poh_recorder.rs index 84f52d7f40330b..2e389ae84256b4 100644 --- a/core/src/poh_recorder.rs +++ b/core/src/poh_recorder.rs @@ -232,6 +232,7 @@ impl PohRecorder { working_bank.bank.slot() ); self.start_slot = working_bank.max_tick_height / working_bank.bank.ticks_per_slot(); + self.start_tick = (self.start_slot + 1) * working_bank.bank.ticks_per_slot(); self.clear_bank(); } if e.is_err() { diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index cd31f4c7119ae7..91bce1aaf02b8b 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -194,6 +194,25 @@ impl ReplayStage { trace!("{} checking poh slot {}", my_id, poh_slot); if blocktree.meta(poh_slot).unwrap().is_some() { // We've already broadcasted entries for this slot, skip it + + // Since we are skipping our leader slot, let's tell poh recorder when we should be + // leader again + if reached_leader_tick { + let _ = bank_forks.read().unwrap().get(poh_slot).map(|bank| { + let next_leader_slot = + leader_schedule_utils::next_leader_slot(&my_id, bank.slot(), &bank); + let mut poh = poh_recorder.lock().unwrap(); + let start_slot = poh.start_slot(); + poh.reset( + bank.tick_height(), + bank.last_blockhash(), + start_slot, + next_leader_slot, + bank.ticks_per_slot(), + ); + }); + } + return; } if bank_forks.read().unwrap().get(poh_slot).is_none() { diff --git a/core/src/replicator.rs b/core/src/replicator.rs index 616725c6fda2bf..f7738178ced7b3 100644 --- a/core/src/replicator.rs +++ b/core/src/replicator.rs @@ -22,7 +22,8 @@ use solana_client::thin_client::{create_client, ThinClient}; use solana_drone::drone::{request_airdrop_transaction, DRONE_PORT}; use solana_sdk::hash::{Hash, Hasher}; use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; -use solana_storage_api::StorageTransaction; +use solana_sdk::transaction::Transaction; +use solana_storage_api::storage_instruction::StorageInstruction; use std::fs::File; use std::io; use std::io::BufReader; @@ -403,14 +404,13 @@ impl Replicator { ); Self::get_airdrop_lamports(&client, &self.keypair, &self.cluster_entrypoint); - let blockhash = client.get_recent_blockhash().expect("blockhash"); - let mut tx = StorageTransaction::new_mining_proof( - &self.keypair, + let ix = StorageInstruction::new_mining_proof( + &self.keypair.pubkey(), self.hash, - blockhash, self.slot, Signature::new(self.signature.as_ref()), ); + let mut tx = Transaction::new(vec![ix]); client .retry_transfer(&self.keypair, &mut tx, 10) .expect("transfer didn't work!"); diff --git a/core/src/sigverify.rs b/core/src/sigverify.rs index 2bbe338ec5319d..159c247658418f 100644 --- a/core/src/sigverify.rs +++ b/core/src/sigverify.rs @@ -337,10 +337,11 @@ mod tests { use bincode::{deserialize, serialize}; use solana_budget_api; use solana_sdk::hash::Hash; + use solana_sdk::instruction::CompiledInstruction; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::system_instruction::SystemInstruction; use solana_sdk::system_program; - use solana_sdk::transaction::{CompiledInstruction, Transaction}; + use solana_sdk::transaction::Transaction; const SIG_OFFSET: usize = std::mem::size_of::() + 1; diff --git a/core/src/storage_stage.rs b/core/src/storage_stage.rs index 4e99c8dfa77f92..73421827d5e498 100644 --- a/core/src/storage_stage.rs +++ b/core/src/storage_stage.rs @@ -15,9 +15,9 @@ use rand_chacha::ChaChaRng; use solana_client::thin_client::create_client_with_timeout; use solana_sdk::hash::Hash; use solana_sdk::pubkey::Pubkey; -use solana_sdk::signature::{Keypair, Signature}; +use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; use solana_sdk::transaction::Transaction; -use solana_storage_api::{self, StorageProgram, StorageTransaction}; +use solana_storage_api::storage_instruction::StorageInstruction; use std::collections::HashSet; use std::io; use std::mem::size_of; @@ -277,7 +277,7 @@ impl StorageStage { Err(io::Error::new(io::ErrorKind::Other, "other failure")) } - pub fn process_entry_crossing( + fn process_entry_crossing( state: &Arc>, keypair: &Arc, _blocktree: &Arc, @@ -288,12 +288,12 @@ impl StorageStage { let mut seed = [0u8; 32]; let signature = keypair.sign(&entry_id.as_ref()); - let tx = StorageTransaction::new_advertise_recent_blockhash( - keypair, + let ix = StorageInstruction::new_advertise_recent_blockhash( + &keypair.pubkey(), entry_id, - Hash::default(), entry_height, ); + let tx = Transaction::new(vec![ix]); tx_sender.send(tx)?; seed.copy_from_slice(&signature.as_ref()[..32]); @@ -354,7 +354,7 @@ impl StorageStage { Ok(()) } - pub fn process_entries( + fn process_entries( keypair: &Arc, storage_state: &Arc>, entry_receiver: &EntryReceiver, @@ -374,7 +374,7 @@ impl StorageStage { for (i, program_id) in tx.program_ids.iter().enumerate() { if solana_storage_api::check_id(&program_id) { match deserialize(&tx.instructions[i].data) { - Ok(StorageProgram::SubmitMiningProof { + Ok(StorageInstruction::SubmitMiningProof { entry_height: proof_entry_height, signature, .. @@ -453,23 +453,17 @@ impl Service for StorageStage { #[cfg(test)] mod tests { + use super::*; use crate::blocktree::{create_new_tmp_ledger, Blocktree}; use crate::cluster_info::ClusterInfo; use crate::contact_info::ContactInfo; use crate::entry::{make_tiny_test_entries, Entry}; use crate::service::Service; - use crate::storage_stage::StorageState; - use crate::storage_stage::NUM_IDENTITIES; - use crate::storage_stage::{ - get_identity_index_from_signature, StorageStage, STORAGE_ROTATE_TEST_COUNT, - }; use rayon::prelude::*; use solana_sdk::genesis_block::GenesisBlock; - use solana_sdk::hash::Hash; - use solana_sdk::hash::Hasher; + use solana_sdk::hash::{Hash, Hasher}; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, KeypairUtil}; - use solana_storage_api::StorageTransaction; use std::cmp::{max, min}; use std::fs::remove_dir_all; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -609,16 +603,17 @@ mod tests { reference_keys = vec![0; keys.len()]; reference_keys.copy_from_slice(keys); } - let mut mining_txs: Vec<_> = Vec::new(); + let keypair = Keypair::new(); - let mining_proof_tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), + let mining_proof_ix = StorageInstruction::new_mining_proof( + &keypair.pubkey(), Hash::default(), 0, keypair.sign_message(b"test"), ); - mining_txs.push(mining_proof_tx); + let mining_proof_tx = Transaction::new(vec![mining_proof_ix]); + let mining_txs = vec![mining_proof_tx]; + let proof_entries = vec![Entry::new(&Hash::default(), 1, mining_txs)]; storage_entry_sender.send(proof_entries).unwrap(); diff --git a/core/tests/local_cluster.rs b/core/tests/local_cluster.rs index 44a345cdc75ec1..24346a109040e0 100644 --- a/core/tests/local_cluster.rs +++ b/core/tests/local_cluster.rs @@ -1,11 +1,12 @@ extern crate solana; +use solana::cluster::Cluster; use solana::cluster_tests; use solana::fullnode::FullnodeConfig; use solana::gossip_service::discover; use solana::local_cluster::LocalCluster; use solana::poh_service::PohServiceConfig; -use std::thread::sleep; +use solana_sdk::timing; use std::time::Duration; #[test] @@ -93,14 +94,16 @@ fn test_two_unbalanced_stakes() { num_ticks_per_slot, num_slots_per_epoch, ); - let num_epochs_to_sleep = 10; - let num_ticks_to_sleep = num_epochs_to_sleep * num_ticks_per_slot * num_slots_per_epoch; - sleep(Duration::from_millis( - num_ticks_to_sleep / num_ticks_per_second as u64 * 100, - )); + cluster_tests::sleep_n_epochs( + 10.0, + &fullnode_config.tick_config, + num_ticks_per_slot, + num_slots_per_epoch, + ); cluster.close_preserve_ledgers(); - let leader_ledger = cluster.ledger_paths[1].clone(); + let leader_id = cluster.entry_point_info.id; + let leader_ledger = cluster.fullnode_infos[&leader_id].ledger_path.clone(); cluster_tests::verify_ledger_ticks(&leader_ledger, num_ticks_per_slot as usize); } @@ -122,3 +125,32 @@ fn test_forwarding() { // Confirm that transactions were forwarded to and processed by the leader. cluster_tests::send_many_transactions(&validator_info, &cluster.funding_keypair, 20); } + +#[test] +fn test_restart_node() { + let fullnode_config = FullnodeConfig::default(); + let slots_per_epoch = 8; + let ticks_per_slot = 16; + let mut cluster = LocalCluster::new_with_tick_config( + &[3], + 100, + &fullnode_config, + ticks_per_slot, + slots_per_epoch, + ); + let nodes = cluster.get_node_ids(); + cluster_tests::sleep_n_epochs( + 1.0, + &fullnode_config.tick_config, + timing::DEFAULT_TICKS_PER_SLOT, + slots_per_epoch, + ); + cluster.restart_node(nodes[0]); + cluster_tests::sleep_n_epochs( + 0.5, + &fullnode_config.tick_config, + timing::DEFAULT_TICKS_PER_SLOT, + slots_per_epoch, + ); + cluster_tests::send_many_transactions(&cluster.entry_point_info, &cluster.funding_keypair, 1); +} diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 542cd1a35e524f..9de0c6172b4aa4 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -17,6 +17,8 @@ solana-budget-api = { path = "../programs/budget_api", version = "0.13.0" } solana-storage-api = { path = "../programs/storage_api", version = "0.13.0" } solana-token-api = { path = "../programs/token_api", version = "0.13.0" } solana-config-api = { path = "../programs/config_api", version = "0.13.0" } +solana-exchange-api = { path = "../programs/exchange_api", version = "0.13.0" } + [dev-dependencies] hashbrown = "0.1.8" diff --git a/genesis/src/main.rs b/genesis/src/main.rs index c2ed7e13499b5d..6517e98b894631 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -79,6 +79,10 @@ fn main() -> Result<(), Box> { ), ("solana_token_program".to_string(), solana_token_api::id()), ("solana_config_program".to_string(), solana_config_api::id()), + ( + "solana_exchange_program".to_string(), + solana_exchange_api::id(), + ), ]); create_new_ledger(ledger_path, &genesis_block)?; @@ -124,6 +128,10 @@ mod tests { 133, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); + let exchange = Pubkey::new(&[ + 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]); assert_eq!(solana_sdk::system_program::id(), system); assert_eq!(solana_sdk::native_loader::id(), native); @@ -133,6 +141,7 @@ mod tests { assert_eq!(solana_token_api::id(), token); assert_eq!(solana_vote_api::id(), vote); assert_eq!(solana_config_api::id(), config); + assert_eq!(solana_exchange_api::id(), exchange); } #[test] @@ -147,6 +156,7 @@ mod tests { solana_token_api::id(), solana_vote_api::id(), solana_config_api::id(), + solana_exchange_api::id(), ]; assert!(ids.into_iter().all(move |id| unique.insert(id))); } diff --git a/install/solana-install-init.sh b/install/solana-install-init.sh index 2b5939f8fdd4ba..ecd2a3e1d39c2a 100644 --- a/install/solana-install-init.sh +++ b/install/solana-install-init.sh @@ -13,6 +13,7 @@ # install solana-install. It just does platform detection, downloads the installer # and runs it. +{ # this ensures the entire script is downloaded # if [ -z "$SOLANA_DOWNLOAD_ROOT" ]; then SOLANA_DOWNLOAD_ROOT="https://github.com/solana-labs/solana/releases/download/" @@ -171,3 +172,5 @@ downloader() { } main "$@" + +} # this ensures the entire script is downloaded # diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index 8a4a96bbe647a8..d162d6befcdc83 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -5,10 +5,10 @@ use libc::c_char; use log::*; use solana_rbpf::{EbpfVmRaw, MemoryRegion}; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::loader_instruction::LoaderInstruction; use solana_sdk::pubkey::Pubkey; use solana_sdk::solana_entrypoint; -use solana_sdk::transaction::InstructionError; use std::ffi::CStr; use std::io::prelude::*; use std::io::{Error, ErrorKind}; diff --git a/programs/budget_api/src/budget_instruction.rs b/programs/budget_api/src/budget_instruction.rs index 4b1b8825c069f2..5be7b38121dfa5 100644 --- a/programs/budget_api/src/budget_instruction.rs +++ b/programs/budget_api/src/budget_instruction.rs @@ -2,8 +2,8 @@ use crate::budget_expr::BudgetExpr; use crate::id; use chrono::prelude::{DateTime, Utc}; use serde_derive::{Deserialize, Serialize}; +use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::{AccountMeta, Instruction}; /// A smart contract. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] diff --git a/programs/budget_api/src/budget_processor.rs b/programs/budget_api/src/budget_processor.rs index a32ccaf27a7d75..48a24e0f54c3fd 100644 --- a/programs/budget_api/src/budget_processor.rs +++ b/programs/budget_api/src/budget_processor.rs @@ -6,8 +6,8 @@ use bincode::{deserialize, serialize}; use chrono::prelude::{DateTime, Utc}; use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::InstructionError; /// Process a Witness Signature. Any payment plans waiting on this signature /// will progress one step. @@ -149,8 +149,9 @@ mod tests { use solana_runtime::bank::Bank; use solana_runtime::bank_client::BankClient; use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::instruction::InstructionError; use solana_sdk::signature::{Keypair, KeypairUtil}; - use solana_sdk::transaction::{InstructionError, Transaction, TransactionError}; + use solana_sdk::transaction::{Transaction, TransactionError}; fn create_bank(lamports: u64) -> (Bank, Keypair) { let (genesis_block, mint_keypair) = GenesisBlock::new(lamports); diff --git a/programs/budget_api/src/budget_state.rs b/programs/budget_api/src/budget_state.rs index 77dbb84aaf453d..cf840e85f5425f 100644 --- a/programs/budget_api/src/budget_state.rs +++ b/programs/budget_api/src/budget_state.rs @@ -2,7 +2,7 @@ use crate::budget_expr::BudgetExpr; use bincode::{self, deserialize, serialize_into}; use serde_derive::{Deserialize, Serialize}; -use solana_sdk::transaction::InstructionError; +use solana_sdk::instruction::InstructionError; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum BudgetError { diff --git a/programs/config_api/src/config_instruction.rs b/programs/config_api/src/config_instruction.rs index f3fdca44deeba3..228611af03e93f 100644 --- a/programs/config_api/src/config_instruction.rs +++ b/programs/config_api/src/config_instruction.rs @@ -1,8 +1,8 @@ use crate::id; use crate::ConfigState; +use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_instruction::SystemInstruction; -use solana_sdk::transaction::{AccountMeta, Instruction}; pub struct ConfigInstruction {} diff --git a/programs/config_api/src/config_processor.rs b/programs/config_api/src/config_processor.rs index c24dd7a1997508..8bece35eea11fc 100644 --- a/programs/config_api/src/config_processor.rs +++ b/programs/config_api/src/config_processor.rs @@ -2,8 +2,8 @@ use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::InstructionError; pub fn process_instruction( _program_id: &Pubkey, diff --git a/programs/exchange_api/Cargo.toml b/programs/exchange_api/Cargo.toml new file mode 100644 index 00000000000000..662d813fdd39ab --- /dev/null +++ b/programs/exchange_api/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "solana-exchange-api" +version = "0.13.0" +description = "Solana Exchange program API" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +log = "0.4.2" +bincode = "1.1.2" +serde = "1.0.89" +serde_derive = "1.0.89" +solana-logger = { path = "../../logger", version = "0.13.0" } +solana-sdk = { path = "../../sdk", version = "0.13.0" } + +[dev-dependencies] +solana-runtime = { path = "../../runtime", version = "0.13.0" } + +[lib] +name = "solana_exchange_api" +crate-type = ["lib"] diff --git a/programs/exchange_api/src/exchange_instruction.rs b/programs/exchange_api/src/exchange_instruction.rs new file mode 100644 index 00000000000000..9040781e3bb3ae --- /dev/null +++ b/programs/exchange_api/src/exchange_instruction.rs @@ -0,0 +1,139 @@ +//! 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 TradeRequestInfo { + /// 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, +} + +#[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), + /// Trade request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - Token account associated with this trade + TradeRequest(TradeRequestInfo), + /// Trade cancellation + /// key 0 - Signer + /// key 1 -Ttrade order to cancel + TradeCancellation, + /// Trade swap request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - 'To' trade order + /// key 3 - `From` trade 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 brokers profit from the swap. + SwapRequest, +} +impl ExchangeInstruction { + pub fn new_account_request(owner: &Pubkey, new: &Pubkey) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*new, false), + ]; + Instruction::new(id(), &ExchangeInstruction::AccountRequest, account_metas) + } + + pub fn new_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( + id(), + &ExchangeInstruction::TransferRequest(token, tokens), + account_metas, + ) + } + + pub fn new_trade_request( + owner: &Pubkey, + trade: &Pubkey, + direction: Direction, + pair: TokenPair, + tokens: u64, + price: u64, + src_account: &Pubkey, + dst_account: &Pubkey, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*trade, false), + AccountMeta::new(*src_account, false), + ]; + Instruction::new( + id(), + &ExchangeInstruction::TradeRequest(TradeRequestInfo { + direction, + pair, + tokens, + price, + dst_account: *dst_account, + }), + account_metas, + ) + } + + pub fn new_trade_cancellation(owner: &Pubkey, trade: &Pubkey, account: &Pubkey) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*trade, false), + AccountMeta::new(*account, false), + ]; + Instruction::new(id(), &ExchangeInstruction::TradeCancellation, account_metas) + } + + pub fn new_swap_request( + owner: &Pubkey, + swap: &Pubkey, + to_trade: &Pubkey, + from_trade: &Pubkey, + to_trade_account: &Pubkey, + from_trade_account: &Pubkey, + profit_account: &Pubkey, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*swap, false), + AccountMeta::new(*to_trade, false), + AccountMeta::new(*from_trade, false), + AccountMeta::new(*to_trade_account, false), + AccountMeta::new(*from_trade_account, false), + AccountMeta::new(*profit_account, false), + ]; + Instruction::new(id(), &ExchangeInstruction::SwapRequest, account_metas) + } +} diff --git a/programs/exchange_api/src/exchange_processor.rs b/programs/exchange_api/src/exchange_processor.rs new file mode 100644 index 00000000000000..33f5647afe5562 --- /dev/null +++ b/programs/exchange_api/src/exchange_processor.rs @@ -0,0 +1,824 @@ +//! Config processor + +use crate::exchange_instruction::*; +use crate::exchange_state::*; +use crate::id; +use log::*; +use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; +use solana_sdk::pubkey::Pubkey; +use std::cmp; + +pub struct ExchangeProcessor {} + +impl ExchangeProcessor { + #[allow(clippy::needless_pass_by_value)] + fn map_to_invalid_arg(err: std::boxed::Box) -> InstructionError { + warn!("Deserialze failed: {:?}", err); + InstructionError::InvalidArgument + } + + fn is_account_unallocated(data: &[u8]) -> Result<(), InstructionError> { + let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + match state { + ExchangeState::Unallocated => Ok(()), + _ => { + error!("New account is already in use"); + Err(InstructionError::InvalidAccountData)? + } + } + } + + fn deserialize_account(data: &[u8]) -> Result<(TokenAccountInfo), InstructionError> { + let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + match state { + ExchangeState::Account(account) => Ok(account), + _ => { + error!("Not a valid account"); + Err(InstructionError::InvalidAccountData)? + } + } + } + + fn deserialize_trade(data: &[u8]) -> Result<(TradeOrderInfo), InstructionError> { + let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + match state { + ExchangeState::Trade(info) => Ok(info), + _ => { + 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(InstructionError::GenericError)? + } + } + } + + fn calculate_swap( + scaler: u64, + swap: &mut TradeSwapInfo, + to_trade: &mut TradeOrderInfo, + from_trade: &mut TradeOrderInfo, + to_trade_account: &mut TokenAccountInfo, + from_trade_account: &mut TokenAccountInfo, + profit_account: &mut TokenAccountInfo, + ) -> Result<(), InstructionError> { + if to_trade.tokens == 0 || from_trade.tokens == 0 { + error!("Inactive Trade, balance is zero"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.price == 0 || from_trade.price == 0 { + error!("Inactive Trade, price is zero"); + 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"); + 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.primary(); + let secondary_token = from_trade.pair.secondary(); + + // Update tokens/accounts + + if to_trade.tokens < primary_cost { + error!("Not enough tokens in to account"); + Err(InstructionError::InvalidArgument)? + } + if from_trade.tokens < secondary_cost { + error!("Not enough tokens in from account"); + Err(InstructionError::InvalidArgument)? + } + to_trade.tokens -= primary_cost; + from_trade.tokens -= secondary_cost; + + to_trade_account.tokens[secondary_token] += secondary_tokens; + from_trade_account.tokens[primary_token] += primary_tokens; + + profit_account.tokens[primary_token] += primary_profit; + profit_account.tokens[secondary_token] += secondary_profit; + + swap.pair = to_trade.pair; + swap.primary_tokens = primary_cost; + swap.primary_price = to_trade.price; + swap.secondary_tokens = secondary_cost; + swap.secondary_price = from_trade.price; + + Ok(()) + } + + fn do_account_request(ka: &mut [KeyedAccount]) -> Result<(), InstructionError> { + if ka.len() < 2 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + + Self::is_account_unallocated(&ka[1].account.data[..])?; + Self::serialize( + &ExchangeState::Account(TokenAccountInfo::default().owner(&ka[0].unsigned_key())), + &mut ka[1].account.data[..], + ) + } + + fn do_transfer_request( + ka: &mut [KeyedAccount], + token: Token, + tokens: u64, + ) -> Result<(), InstructionError> { + if ka.len() < 3 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + + let mut to_account = Self::deserialize_account(&ka[1].account.data[..])?; + + if &id() == ka[2].unsigned_key() { + to_account.tokens[token] += tokens; + } else { + let mut from_account = Self::deserialize_account(&ka[2].account.data[..])?; + + if &from_account.owner != ka[0].unsigned_key() { + error!("Signer does not own from account"); + Err(InstructionError::GenericError)? + } + + if from_account.tokens[token] < tokens { + error!("From account balance too low"); + Err(InstructionError::GenericError)? + } + + from_account.tokens[token] -= tokens; + to_account.tokens[token] += tokens; + + Self::serialize( + &ExchangeState::Account(from_account), + &mut ka[1].account.data[..], + )?; + } + + Self::serialize( + &ExchangeState::Account(to_account), + &mut ka[1].account.data[..], + ) + } + + fn do_trade_request( + ka: &mut [KeyedAccount], + info: TradeRequestInfo, + ) -> Result<(), InstructionError> { + if ka.len() < 3 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + + Self::is_account_unallocated(&ka[1].account.data[..])?; + + let mut account = Self::deserialize_account(&ka[2].account.data[..])?; + + if &account.owner != ka[0].unsigned_key() { + error!("Signer does not own account"); + Err(InstructionError::GenericError)? + } + let from_token = match info.direction { + Direction::To => info.pair.primary(), + Direction::From => info.pair.secondary(), + }; + if account.tokens[from_token] < info.tokens { + error!("From token balance is too low"); + Err(InstructionError::GenericError)? + } + + if let Err(e) = check_trade(info.direction, info.tokens, info.price) { + bincode::serialize(&e).unwrap(); + } + + // Trade holds the tokens in escrow + account.tokens[from_token] -= info.tokens; + + Self::serialize( + &ExchangeState::Trade(TradeOrderInfo { + owner: *ka[0].unsigned_key(), + direction: info.direction, + pair: info.pair, + tokens: info.tokens, + price: info.price, + src_account: *ka[2].unsigned_key(), + dst_account: info.dst_account, + }), + &mut ka[1].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(account), + &mut ka[2].account.data[..], + ) + } + + fn do_trade_cancellation(ka: &mut [KeyedAccount]) -> Result<(), InstructionError> { + if ka.len() < 3 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + let mut trade = Self::deserialize_trade(&ka[1].account.data[..])?; + let mut account = Self::deserialize_account(&ka[2].account.data[..])?; + + if &trade.owner != ka[0].unsigned_key() { + error!("Signer does not own trade"); + Err(InstructionError::GenericError)? + } + + if &account.owner != ka[0].unsigned_key() { + error!("Signer does not own account"); + Err(InstructionError::GenericError)? + } + + let token = match trade.direction { + Direction::To => trade.pair.primary(), + Direction::From => trade.pair.secondary(), + }; + + // Outstanding tokens transferred back to account + account.tokens[token] += trade.tokens; + // Trade becomes invalid + trade.tokens = 0; + + Self::serialize(&ExchangeState::Trade(trade), &mut ka[1].account.data[..])?; + Self::serialize( + &ExchangeState::Account(account), + &mut ka[2].account.data[..], + ) + } + + fn do_swap_request(ka: &mut [KeyedAccount]) -> Result<(), InstructionError> { + if ka.len() < 7 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + Self::is_account_unallocated(&ka[1].account.data[..])?; + let mut to_trade = Self::deserialize_trade(&ka[2].account.data[..])?; + let mut from_trade = Self::deserialize_trade(&ka[3].account.data[..])?; + let mut to_trade_account = Self::deserialize_account(&ka[4].account.data[..])?; + let mut from_trade_account = Self::deserialize_account(&ka[5].account.data[..])?; + let mut profit_account = Self::deserialize_account(&ka[6].account.data[..])?; + + if &to_trade.dst_account != ka[4].unsigned_key() { + error!("To trade account and to account differ"); + Err(InstructionError::InvalidArgument)? + } + if &from_trade.dst_account != ka[5].unsigned_key() { + error!("From trade account and from account differ"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.direction != Direction::To { + error!("To trade is not a To"); + Err(InstructionError::InvalidArgument)? + } + if from_trade.direction != Direction::From { + error!("From trade is not a From"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.pair != from_trade.pair { + error!("Mismatched token pairs"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.direction == from_trade.direction { + error!("Matching trade directions"); + Err(InstructionError::InvalidArgument)? + } + + let mut swap = TradeSwapInfo::default(); + swap.to_trade_order = *ka[2].unsigned_key(); + swap.from_trade_order = *ka[3].unsigned_key(); + + if let Err(e) = Self::calculate_swap( + SCALER, + &mut swap, + &mut to_trade, + &mut from_trade, + &mut to_trade_account, + &mut from_trade_account, + &mut profit_account, + ) { + error!( + "Swap calculation failed from {} for {} to {} for {}", + from_trade.tokens, from_trade.price, to_trade.tokens, to_trade.price, + ); + Err(e)? + } + + Self::serialize(&ExchangeState::Swap(swap), &mut ka[1].account.data[..])?; + Self::serialize(&ExchangeState::Trade(to_trade), &mut ka[2].account.data[..])?; + Self::serialize( + &ExchangeState::Trade(from_trade), + &mut ka[3].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(to_trade_account), + &mut ka[4].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(from_trade_account), + &mut ka[5].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(profit_account), + &mut ka[6].account.data[..], + ) + } +} + +pub fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], + _tick_height: u64, +) -> Result<(), InstructionError> { + let command = bincode::deserialize::(data).map_err(|err| { + info!("Invalid transaction data: {:?} {:?}", data, err); + InstructionError::InvalidInstructionData + })?; + + trace!("{:?}", command); + + match command { + ExchangeInstruction::AccountRequest => { + ExchangeProcessor::do_account_request(keyed_accounts) + } + ExchangeInstruction::TransferRequest(token, tokens) => { + ExchangeProcessor::do_transfer_request(keyed_accounts, token, tokens) + } + ExchangeInstruction::TradeRequest(info) => { + ExchangeProcessor::do_trade_request(keyed_accounts, info) + } + ExchangeInstruction::TradeCancellation => { + ExchangeProcessor::do_trade_cancellation(keyed_accounts) + } + ExchangeInstruction::SwapRequest => ExchangeProcessor::do_swap_request(keyed_accounts), + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_runtime::bank::Bank; + use solana_runtime::bank_client::BankClient; + use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::signature::{Keypair, KeypairUtil}; + use solana_sdk::system_instruction::SystemInstruction; + use std::mem; + + 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_account_tokens: Tokens, + secondary_account_tokens: Tokens, + profit_account_tokens: Tokens, + ) -> Result<(), InstructionError> { + trace!( + "Swap {} for {} to {} for {}", + primary_tokens, + primary_price, + secondary_tokens, + secondary_price, + ); + let mut swap = TradeSwapInfo::default(); + let mut to_trade = TradeOrderInfo::default(); + let mut from_trade = TradeOrderInfo::default().direction(Direction::From); + let mut to_account = TokenAccountInfo::default(); + let mut from_account = TokenAccountInfo::default(); + 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 swap, + &mut to_trade, + &mut from_trade, + &mut to_account, + &mut from_account, + &mut profit_account, + )?; + + trace!( + "{:?} {:?} {:?} {:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}", + to_trade.tokens, + primary_tokens_expect, + from_trade.tokens, + secondary_tokens_expect, + to_account.tokens, + primary_account_tokens, + from_account.tokens, + secondary_account_tokens, + 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_account.tokens, primary_account_tokens); + assert_eq!(from_account.tokens, secondary_account_tokens); + assert_eq!(profit_account.tokens, profit_account_tokens); + assert_eq!(swap.primary_tokens, primary_tokens - to_trade.tokens); + assert_eq!(swap.primary_price, to_trade.price); + assert_eq!(swap.secondary_tokens, secondary_tokens - from_trade.tokens); + assert_eq!(swap.secondary_price, from_trade.price); + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn test_calculate_swap() { + solana_logger::setup(); + + try_calc(1, 50, 2, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 50, 1, 0, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 0, 1, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 50, 1, 50, 0, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 50, 0, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 1, 2, 2, 3, 1, 2, Tokens::new(0, 0, 0, 0), Tokens::new( 0, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + + try_calc(1, 50, 1, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap(); + try_calc(1, 1, 2, 3, 3, 0, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 2, 2, 3, 3, 1, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 3, 2, 3, 3, 2, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 3, 2, 6, 3, 1, 0, Tokens::new(0, 4, 0, 0), Tokens::new( 2, 0, 0, 0), Tokens::new( 0, 2, 0, 0)).unwrap(); + try_calc(1000, 1, 2000, 3, 3000, 0, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 3, 2, 7, 3, 1, 1, Tokens::new(0, 4, 0, 0), Tokens::new( 2, 0, 0, 0), Tokens::new( 0, 2, 0, 0)).unwrap(); + try_calc(1000, 3000, 333, 1000, 500, 0, 1, Tokens::new(0, 999, 0, 0), Tokens::new(1998, 0, 0, 0), Tokens::new(1002, 0, 0, 0)).unwrap(); + try_calc(1000, 50, 100, 50, 101, 0,45, Tokens::new(0, 5, 0, 0), Tokens::new( 49, 0, 0, 0), Tokens::new( 1, 0, 0, 0)).unwrap(); + } + + fn create_bank(lamports: u64) -> (Bank, Keypair) { + let (genesis_block, mint_keypair) = GenesisBlock::new(lamports); + let mut bank = Bank::new(&genesis_block); + bank.add_instruction_processor(id(), process_instruction); + (bank, mint_keypair) + } + + fn create_client(bank: &Bank, mint_keypair: Keypair) -> (BankClient, Pubkey) { + let owner = Keypair::new(); + let pubkey = owner.pubkey(); + let mint_client = BankClient::new(&bank, mint_keypair); + mint_client + .process_instruction(SystemInstruction::new_move( + &mint_client.pubkey(), + &owner.pubkey(), + 42, + )) + .expect("new_move"); + + let client = BankClient::new(&bank, owner); + + (client, pubkey) + } + + fn create_account(client: &BankClient, owner: &Pubkey) -> Pubkey { + let new = Keypair::new().pubkey(); + let instruction = SystemInstruction::new_program_account( + &owner, + &new, + 1, + mem::size_of::() as u64, + &id(), + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + new + } + + fn create_token_account(client: &BankClient, owner: &Pubkey) -> Pubkey { + let new = Keypair::new().pubkey(); + let instruction = SystemInstruction::new_program_account( + &owner, + &new, + 1, + mem::size_of::() as u64, + &id(), + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + let instruction = ExchangeInstruction::new_account_request(&owner, &new); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + new + } + + fn transfer(client: &BankClient, owner: &Pubkey, to: &Pubkey, token: Token, tokens: u64) { + let instruction = + ExchangeInstruction::new_transfer_request(owner, to, &id(), token, tokens); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + } + + fn trade( + client: &BankClient, + owner: &Pubkey, + direction: Direction, + pair: TokenPair, + from_token: Token, + src_tokens: u64, + trade_tokens: u64, + price: u64, + ) -> (Pubkey, Pubkey, Pubkey) { + let trade = create_account(&client, &owner); + let src = create_token_account(&client, &owner); + let dst = create_token_account(&client, &owner); + transfer(&client, &owner, &src, from_token, src_tokens); + + let instruction = ExchangeInstruction::new_trade_request( + owner, + &trade, + direction, + pair, + trade_tokens, + price, + &src, + &dst, + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + (trade, src, dst) + } + + fn deserialize_swap(data: &[u8]) -> TradeSwapInfo { + let state: ExchangeState = + bincode::deserialize(data).expect(&format!("{}:{}", line!(), file!())); + match state { + ExchangeState::Swap(info) => info, + _ => panic!("Not a valid swap"), + } + } + + #[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 = bank.get_account(&new).unwrap(); + + // Check results + + assert_eq!( + TokenAccountInfo::default().owner(&owner), + 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 = ExchangeInstruction::new_account_request(&owner, &new); + client + .process_instruction(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 = + ExchangeInstruction::new_transfer_request(&owner, &new, &id(), Token::A, 42); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + + let new_account = bank.get_account(&new).unwrap(); + + // Check results + + assert_eq!( + TokenAccountInfo::default() + .owner(&owner) + .tokens(42, 0, 0, 0), + 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, dst) = trade( + &client, + &owner, + Direction::To, + TokenPair::AB, + Token::A, + 42, + 2, + 1000, + ); + + let trade_account = bank.get_account(&trade).unwrap(); + let src_account = bank.get_account(&src).unwrap(); + let dst_account = bank.get_account(&dst).unwrap(); + + // check results + + assert_eq!( + TradeOrderInfo { + owner: owner, + direction: Direction::To, + pair: TokenPair::AB, + tokens: 2, + price: 1000, + src_account: src, + dst_account: dst + }, + ExchangeProcessor::deserialize_trade(&trade_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default() + .owner(&owner) + .tokens(40, 0, 0, 0), + ExchangeProcessor::deserialize_account(&src_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 0, 0, 0), + ExchangeProcessor::deserialize_account(&dst_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 swap = create_account(&client, &owner); + let profit = create_token_account(&client, &owner); + let (to_trade, to_src, to_dst) = trade( + &client, + &owner, + Direction::To, + TokenPair::AB, + Token::A, + 2, + 2, + 2000, + ); + let (from_trade, from_src, from_dst) = trade( + &client, + &owner, + Direction::From, + TokenPair::AB, + Token::B, + 3, + 3, + 3000, + ); + + let instruction = ExchangeInstruction::new_swap_request( + &owner, + &swap, + &to_trade, + &from_trade, + &to_dst, + &from_dst, + &profit, + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + + let to_trade_account = bank.get_account(&to_trade).unwrap(); + let to_src_account = bank.get_account(&to_src).unwrap(); + let to_dst_account = bank.get_account(&to_dst).unwrap(); + let from_trade_account = bank.get_account(&from_trade).unwrap(); + let from_src_account = bank.get_account(&from_src).unwrap(); + let from_dst_account = bank.get_account(&from_dst).unwrap(); + let profit_account = bank.get_account(&profit).unwrap(); + let swap_account = bank.get_account(&swap).unwrap(); + + // check results + + assert_eq!( + TradeOrderInfo { + owner: owner, + direction: Direction::To, + pair: TokenPair::AB, + tokens: 1, + price: 2000, + src_account: to_src, + dst_account: to_dst + }, + ExchangeProcessor::deserialize_trade(&to_trade_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 0, 0, 0), + ExchangeProcessor::deserialize_account(&to_src_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 2, 0, 0), + ExchangeProcessor::deserialize_account(&to_dst_account.data[..]).unwrap() + ); + assert_eq!( + TradeOrderInfo { + owner: owner, + direction: Direction::From, + pair: TokenPair::AB, + tokens: 0, + price: 3000, + src_account: from_src, + dst_account: from_dst + }, + ExchangeProcessor::deserialize_trade(&from_trade_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 0, 0, 0), + ExchangeProcessor::deserialize_account(&from_src_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(1, 0, 0, 0), + ExchangeProcessor::deserialize_account(&from_dst_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 1, 0, 0), + ExchangeProcessor::deserialize_account(&profit_account.data[..]).unwrap() + ); + assert_eq!( + TradeSwapInfo { + pair: TokenPair::AB, + to_trade_order: to_trade, + from_trade_order: from_trade, + primary_tokens: 1, + primary_price: 2000, + secondary_tokens: 3, + secondary_price: 3000, + }, + deserialize_swap(&swap_account.data[..]) + ); + } +} diff --git a/programs/exchange_api/src/exchange_state.rs b/programs/exchange_api/src/exchange_state.rs new file mode 100644 index 00000000000000..762f92814cf361 --- /dev/null +++ b/programs/exchange_api/src/exchange_state.rs @@ -0,0 +1,267 @@ +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() -> Token { + 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 { + Tokens { + 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 enum TokenPair { + AB, + AC, + AD, + BC, + BD, + CD, +} +impl Default for TokenPair { + fn default() -> TokenPair { + TokenPair::AB + } +} +impl TokenPair { + pub fn primary(self) -> Token { + match self { + TokenPair::AB => Token::A, + TokenPair::AC => Token::A, + TokenPair::AD => Token::A, + TokenPair::BC => Token::B, + TokenPair::BD => Token::B, + TokenPair::CD => Token::C, + } + } + pub fn secondary(self) -> Token { + match self { + TokenPair::AB => Token::B, + TokenPair::AC => Token::C, + TokenPair::AD => Token::D, + TokenPair::BC => Token::C, + TokenPair::BD => Token::D, + TokenPair::CD => Token::D, + } + } +} + +/// 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 + } +} + +/// Direction of the exchange between two tokens in a pair +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +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, +} +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Direction::To => write!(f, "T")?, + Direction::From => write!(f, "F")?, + } + Ok(()) + } +} + +/// Trade accounts are populated with this structure +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct TradeOrderInfo { + /// Owner of the trade 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 cancelled. + pub src_account: Pubkey, + /// account which the tokens the tokens will be deposited into on a successful trade + pub dst_account: Pubkey, +} +impl Default for TradeOrderInfo { + fn default() -> TradeOrderInfo { + TradeOrderInfo { + owner: Pubkey::default(), + pair: TokenPair::AB, + direction: Direction::To, + tokens: 0, + price: 0, + src_account: Pubkey::default(), + dst_account: Pubkey::default(), + } + } +} +impl TradeOrderInfo { + pub fn pair(mut self, pair: TokenPair) -> Self { + self.pair = pair; + self + } + pub fn direction(mut self, direction: Direction) -> Self { + self.direction = direction; + 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(direction: Direction, tokens: u64, price: u64) -> Result<(), ExchangeError> { + match direction { + Direction::To => { + if tokens * price / SCALER == 0 { + Err(ExchangeError::InvalidTrade(format!( + "To trade of {} for {}/{} results in 0 tradeable tokens", + tokens, SCALER, price + )))? + } + } + Direction::From => { + if tokens * SCALER / price == 0 { + Err(ExchangeError::InvalidTrade(format!( + "From trade of {} for {}?{} results in 0 tradeable tokens", + tokens, SCALER, price + )))? + } + } + } + Ok(()) +} + +/// Swap accounts are populated with this structure +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct TradeSwapInfo { + /// Pair swapped + pub pair: TokenPair, + /// `To` trade order + pub to_trade_order: Pubkey, + /// `From` trade 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, +} + +/// 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 Userdata is unallocated + Unallocated, + // Token account + Account(TokenAccountInfo), + // Trade order account + Trade(TradeOrderInfo), + // Swap account + Swap(TradeSwapInfo), + Invalid, +} +impl Default for ExchangeState { + fn default() -> ExchangeState { + ExchangeState::Unallocated + } +} diff --git a/programs/exchange_api/src/exchange_transaction.rs b/programs/exchange_api/src/exchange_transaction.rs new file mode 100644 index 00000000000000..35c4418aa39dfd --- /dev/null +++ b/programs/exchange_api/src/exchange_transaction.rs @@ -0,0 +1,123 @@ +use crate::exchange_instruction::*; +use crate::exchange_state::*; +use crate::id; +use solana_sdk::hash::Hash; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::{Keypair, KeypairUtil}; +use solana_sdk::system_instruction::SystemInstruction; +use solana_sdk::transaction::Transaction; +use std::mem; + +pub struct ExchangeTransaction {} + +impl ExchangeTransaction { + pub fn new_account_request( + owner: &Keypair, + new: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let space = mem::size_of::() as u64; + let create_ix = SystemInstruction::new_program_account(owner_id, new, 1, space, &id()); + let request_ix = ExchangeInstruction::new_account_request(owner_id, new); + let mut tx = Transaction::new(vec![create_ix, request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + pub fn new_transfer_request( + owner: &Keypair, + to: &Pubkey, + from: &Pubkey, + token: Token, + tokens: u64, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let request_ix = + ExchangeInstruction::new_transfer_request(owner_id, to, from, token, tokens); + let mut tx = Transaction::new(vec![request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + #[allow(clippy::too_many_arguments)] + pub fn new_trade_request( + owner: &Keypair, + trade: &Pubkey, + direction: Direction, + pair: TokenPair, + tokens: u64, + price: u64, + src_account: &Pubkey, + dst_account: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let space = mem::size_of::() as u64; + let create_ix = SystemInstruction::new_program_account(owner_id, trade, 1, space, &id()); + let request_ix = ExchangeInstruction::new_trade_request( + owner_id, + trade, + direction, + pair, + tokens, + price, + src_account, + dst_account, + ); + let mut tx = Transaction::new(vec![create_ix, request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + pub fn new_trade_cancellation( + owner: &Keypair, + trade: &Pubkey, + account: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let request_ix = ExchangeInstruction::new_trade_cancellation(owner_id, trade, account); + let mut tx = Transaction::new(vec![request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + pub fn new_swap_request( + owner: &Keypair, + swap: &Pubkey, + to_trade: &Pubkey, + from_trade: &Pubkey, + to_trade_account: &Pubkey, + from_trade_account: &Pubkey, + profit_account: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let space = mem::size_of::() as u64; + let create_ix = SystemInstruction::new_program_account(owner_id, swap, 1, space, &id()); + let request_ix = ExchangeInstruction::new_swap_request( + owner_id, + swap, + to_trade, + from_trade, + to_trade_account, + from_trade_account, + profit_account, + ); + let mut tx = Transaction::new(vec![create_ix, request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } +} diff --git a/programs/exchange_api/src/lib.rs b/programs/exchange_api/src/lib.rs new file mode 100644 index 00000000000000..a4505e15120dd7 --- /dev/null +++ b/programs/exchange_api/src/lib.rs @@ -0,0 +1,19 @@ +pub mod exchange_instruction; +pub mod exchange_processor; +pub mod exchange_state; +pub mod exchange_transaction; + +use solana_sdk::pubkey::Pubkey; + +pub const EXCHANGE_PROGRAM_ID: [u8; 32] = [ + 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, +]; + +pub fn check_id(program_id: &Pubkey) -> bool { + program_id.as_ref() == EXCHANGE_PROGRAM_ID +} + +pub fn id() -> Pubkey { + Pubkey::new(&EXCHANGE_PROGRAM_ID) +} diff --git a/programs/exchange_program/Cargo.toml b/programs/exchange_program/Cargo.toml new file mode 100644 index 00000000000000..f5b47e5772df39 --- /dev/null +++ b/programs/exchange_program/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "solana-exchange-program" +version = "0.13.0" +description = "Solana exchange program" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +log = "0.4.2" +solana-exchange-api = { path = "../exchange_api", version = "0.13.0" } +solana-logger = { path = "../../logger", version = "0.13.0" } +solana-sdk = { path = "../../sdk", version = "0.13.0" } + +[lib] +name = "solana_exchange_program" +crate-type = ["cdylib"] + + diff --git a/programs/exchange_program/src/lib.rs b/programs/exchange_program/src/lib.rs new file mode 100644 index 00000000000000..47e6d44ffe641a --- /dev/null +++ b/programs/exchange_program/src/lib.rs @@ -0,0 +1,3 @@ +use solana_exchange_api::exchange_processor::process_instruction; + +solana_sdk::process_instruction_entrypoint!(process_instruction); diff --git a/programs/failure_program/src/lib.rs b/programs/failure_program/src/lib.rs index 8cf8b9a96368e8..afb5abf98ed072 100644 --- a/programs/failure_program/src/lib.rs +++ b/programs/failure_program/src/lib.rs @@ -1,7 +1,7 @@ use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; use solana_sdk::solana_entrypoint; -use solana_sdk::transaction::InstructionError; solana_entrypoint!(entrypoint); fn entrypoint( diff --git a/programs/failure_program/tests/failure.rs b/programs/failure_program/tests/failure.rs index 5f2784feac33e8..11f3c0ad2e17d2 100644 --- a/programs/failure_program/tests/failure.rs +++ b/programs/failure_program/tests/failure.rs @@ -2,8 +2,9 @@ use solana_runtime::bank::Bank; use solana_runtime::bank_client::BankClient; use solana_runtime::loader_utils::{create_invoke_instruction, load_program}; use solana_sdk::genesis_block::GenesisBlock; +use solana_sdk::instruction::InstructionError; use solana_sdk::native_loader; -use solana_sdk::transaction::{InstructionError, TransactionError}; +use solana_sdk::transaction::TransactionError; #[test] fn test_program_native_failure() { diff --git a/programs/noop_program/src/lib.rs b/programs/noop_program/src/lib.rs index 2af2d2437332e7..344c868fad5b7f 100644 --- a/programs/noop_program/src/lib.rs +++ b/programs/noop_program/src/lib.rs @@ -1,8 +1,8 @@ use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; use solana_sdk::solana_entrypoint; -use solana_sdk::transaction::InstructionError; solana_entrypoint!(entrypoint); fn entrypoint( diff --git a/programs/storage_api/src/lib.rs b/programs/storage_api/src/lib.rs index a9435982c00820..11d736ba019fd8 100644 --- a/programs/storage_api/src/lib.rs +++ b/programs/storage_api/src/lib.rs @@ -1,10 +1,8 @@ +pub mod storage_contract; +pub mod storage_instruction; pub mod storage_processor; -use serde_derive::{Deserialize, Serialize}; -use solana_sdk::hash::Hash; use solana_sdk::pubkey::Pubkey; -use solana_sdk::signature::{Keypair, Signature}; -use solana_sdk::transaction::Transaction; pub const ENTRIES_PER_SEGMENT: u64 = 16; @@ -12,58 +10,6 @@ pub fn get_segment_from_entry(entry_height: u64) -> usize { (entry_height / ENTRIES_PER_SEGMENT) as usize } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum ProofStatus { - Valid, - NotValid, - Skipped, -} - -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct ProofInfo { - pub id: Pubkey, - pub signature: Signature, - pub sha_state: Hash, -} - -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct ValidationInfo { - pub id: Pubkey, - pub proof_mask: Vec, -} - -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct StorageProgramState { - pub entry_height: u64, - pub hash: Hash, - - pub proofs: Vec>, - pub previous_proofs: Vec>, - - pub lockout_validations: Vec>, - pub reward_validations: Vec>, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum StorageProgram { - SubmitMiningProof { - sha_state: Hash, - entry_height: u64, - signature: Signature, - }, - AdvertiseStorageRecentBlockhash { - hash: Hash, - entry_height: u64, - }, - ClaimStorageReward { - entry_height: u64, - }, - ProofValidation { - entry_height: u64, - proof_mask: Vec, - }, -} - const STORAGE_PROGRAM_ID: [u8; 32] = [ 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -76,57 +22,3 @@ pub fn check_id(program_id: &Pubkey) -> bool { pub fn id() -> Pubkey { Pubkey::new(&STORAGE_PROGRAM_ID) } - -pub struct StorageTransaction {} - -impl StorageTransaction { - pub fn new_mining_proof( - from_keypair: &Keypair, - sha_state: Hash, - recent_blockhash: Hash, - entry_height: u64, - signature: Signature, - ) -> Transaction { - let program = StorageProgram::SubmitMiningProof { - sha_state, - entry_height, - signature, - }; - Transaction::new_signed(from_keypair, &[], &id(), &program, recent_blockhash, 0) - } - - pub fn new_advertise_recent_blockhash( - from_keypair: &Keypair, - storage_hash: Hash, - recent_blockhash: Hash, - entry_height: u64, - ) -> Transaction { - let program = StorageProgram::AdvertiseStorageRecentBlockhash { - hash: storage_hash, - entry_height, - }; - Transaction::new_signed(from_keypair, &[], &id(), &program, recent_blockhash, 0) - } - - pub fn new_proof_validation( - from_keypair: &Keypair, - recent_blockhash: Hash, - entry_height: u64, - proof_mask: Vec, - ) -> Transaction { - let program = StorageProgram::ProofValidation { - entry_height, - proof_mask, - }; - Transaction::new_signed(from_keypair, &[], &id(), &program, recent_blockhash, 0) - } - - pub fn new_reward_claim( - from_keypair: &Keypair, - recent_blockhash: Hash, - entry_height: u64, - ) -> Transaction { - let program = StorageProgram::ClaimStorageReward { entry_height }; - Transaction::new_signed(from_keypair, &[], &id(), &program, recent_blockhash, 0) - } -} diff --git a/programs/storage_api/src/storage_contract.rs b/programs/storage_api/src/storage_contract.rs new file mode 100644 index 00000000000000..a8f1dd2690d95f --- /dev/null +++ b/programs/storage_api/src/storage_contract.rs @@ -0,0 +1,36 @@ +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::hash::Hash; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::Signature; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum ProofStatus { + Valid, + NotValid, + Skipped, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct ProofInfo { + pub id: Pubkey, + pub signature: Signature, + pub sha_state: Hash, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct ValidationInfo { + pub id: Pubkey, + pub proof_mask: Vec, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct StorageContract { + pub entry_height: u64, + pub hash: Hash, + + pub proofs: Vec>, + pub previous_proofs: Vec>, + + pub lockout_validations: Vec>, + pub reward_validations: Vec>, +} diff --git a/programs/storage_api/src/storage_instruction.rs b/programs/storage_api/src/storage_instruction.rs new file mode 100644 index 00000000000000..d116237c5c48da --- /dev/null +++ b/programs/storage_api/src/storage_instruction.rs @@ -0,0 +1,76 @@ +use crate::id; +use crate::storage_contract::ProofStatus; +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::hash::Hash; +use solana_sdk::instruction::{AccountMeta, Instruction}; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::Signature; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum StorageInstruction { + SubmitMiningProof { + sha_state: Hash, + entry_height: u64, + signature: Signature, + }, + AdvertiseStorageRecentBlockhash { + hash: Hash, + entry_height: u64, + }, + ClaimStorageReward { + entry_height: u64, + }, + ProofValidation { + entry_height: u64, + proof_mask: Vec, + }, +} + +impl StorageInstruction { + pub fn new_mining_proof( + from_pubkey: &Pubkey, + sha_state: Hash, + entry_height: u64, + signature: Signature, + ) -> Instruction { + let storage_instruction = StorageInstruction::SubmitMiningProof { + sha_state, + entry_height, + signature, + }; + let account_metas = vec![AccountMeta::new(*from_pubkey, true)]; + Instruction::new(id(), &storage_instruction, account_metas) + } + + pub fn new_advertise_recent_blockhash( + from_pubkey: &Pubkey, + storage_hash: Hash, + entry_height: u64, + ) -> Instruction { + let storage_instruction = StorageInstruction::AdvertiseStorageRecentBlockhash { + hash: storage_hash, + entry_height, + }; + let account_metas = vec![AccountMeta::new(*from_pubkey, true)]; + Instruction::new(id(), &storage_instruction, account_metas) + } + + pub fn new_proof_validation( + from_pubkey: &Pubkey, + entry_height: u64, + proof_mask: Vec, + ) -> Instruction { + let storage_instruction = StorageInstruction::ProofValidation { + entry_height, + proof_mask, + }; + let account_metas = vec![AccountMeta::new(*from_pubkey, true)]; + Instruction::new(id(), &storage_instruction, account_metas) + } + + pub fn new_reward_claim(from_pubkey: &Pubkey, entry_height: u64) -> Instruction { + let storage_instruction = StorageInstruction::ClaimStorageReward { entry_height }; + let account_metas = vec![AccountMeta::new(*from_pubkey, true)]; + Instruction::new(id(), &storage_instruction, account_metas) + } +} diff --git a/programs/storage_api/src/storage_processor.rs b/programs/storage_api/src/storage_processor.rs index f4dce68068c5c3..913370f48b3271 100644 --- a/programs/storage_api/src/storage_processor.rs +++ b/programs/storage_api/src/storage_processor.rs @@ -2,11 +2,13 @@ //! Receive mining proofs from miners, validate the answers //! and give reward for good proofs. -use crate::*; +use crate::storage_contract::{ProofInfo, ProofStatus, StorageContract, ValidationInfo}; +use crate::storage_instruction::StorageInstruction; +use crate::{get_segment_from_entry, ENTRIES_PER_SEGMENT}; use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::InstructionError; pub const TOTAL_VALIDATOR_REWARDS: u64 = 1000; pub const TOTAL_REPLICATOR_REWARDS: u64 = 1000; @@ -42,33 +44,31 @@ pub fn process_instruction( } if let Ok(syscall) = bincode::deserialize(data) { - let mut storage_account_state = if let Ok(storage_account_state) = - bincode::deserialize(&keyed_accounts[0].account.data) - { - storage_account_state - } else { - StorageProgramState::default() - }; + let mut storage_contract = + if let Ok(storage_contract) = bincode::deserialize(&keyed_accounts[0].account.data) { + storage_contract + } else { + StorageContract::default() + }; debug!( - "deserialized state height: {}", - storage_account_state.entry_height + "deserialized contract height: {}", + storage_contract.entry_height ); match syscall { - StorageProgram::SubmitMiningProof { + StorageInstruction::SubmitMiningProof { sha_state, entry_height, signature, } => { let segment_index = get_segment_from_entry(entry_height); - let current_segment_index = - get_segment_from_entry(storage_account_state.entry_height); + let current_segment_index = get_segment_from_entry(storage_contract.entry_height); if segment_index >= current_segment_index { return Err(InstructionError::InvalidArgument); } debug!( - "Mining proof submitted with state {:?} entry_height: {}", + "Mining proof submitted with contract {:?} entry_height: {}", sha_state, entry_height ); @@ -77,10 +77,10 @@ pub fn process_instruction( sha_state, signature, }; - storage_account_state.proofs[segment_index].push(proof_info); + storage_contract.proofs[segment_index].push(proof_info); } - StorageProgram::AdvertiseStorageRecentBlockhash { hash, entry_height } => { - let original_segments = storage_account_state.entry_height / ENTRIES_PER_SEGMENT; + StorageInstruction::AdvertiseStorageRecentBlockhash { hash, entry_height } => { + let original_segments = storage_contract.entry_height / ENTRIES_PER_SEGMENT; let segments = entry_height / ENTRIES_PER_SEGMENT; debug!( "advertise new last id segments: {} orig: {}", @@ -90,40 +90,39 @@ pub fn process_instruction( return Err(InstructionError::InvalidArgument); } - storage_account_state.entry_height = entry_height; - storage_account_state.hash = hash; + storage_contract.entry_height = entry_height; + storage_contract.hash = hash; // move the proofs to previous_proofs - storage_account_state.previous_proofs = storage_account_state.proofs.clone(); - storage_account_state.proofs.clear(); - storage_account_state + storage_contract.previous_proofs = storage_contract.proofs.clone(); + storage_contract.proofs.clear(); + storage_contract .proofs .resize(segments as usize, Vec::new()); // move lockout_validations to reward_validations - storage_account_state.reward_validations = - storage_account_state.lockout_validations.clone(); - storage_account_state.lockout_validations.clear(); - storage_account_state + storage_contract.reward_validations = storage_contract.lockout_validations.clone(); + storage_contract.lockout_validations.clear(); + storage_contract .lockout_validations .resize(segments as usize, Vec::new()); } - StorageProgram::ProofValidation { + StorageInstruction::ProofValidation { entry_height, proof_mask, } => { - if entry_height >= storage_account_state.entry_height { + if entry_height >= storage_contract.entry_height { return Err(InstructionError::InvalidArgument); } let segment_index = get_segment_from_entry(entry_height); - if storage_account_state.previous_proofs[segment_index].len() != proof_mask.len() { + if storage_contract.previous_proofs[segment_index].len() != proof_mask.len() { return Err(InstructionError::InvalidArgument); } // TODO: Check that each proof mask matches the signature /*for (i, entry) in proof_mask.iter().enumerate() { - if storage_account_state.previous_proofs[segment_index][i] != signature.as_ref[0] { + if storage_contract.previous_proofs[segment_index][i] != signature.as_ref[0] { return Err(InstructionError::InvalidArgument); } }*/ @@ -132,14 +131,14 @@ pub fn process_instruction( id: *keyed_accounts[0].signer_key().unwrap(), proof_mask, }; - storage_account_state.lockout_validations[segment_index].push(info); + storage_contract.lockout_validations[segment_index].push(info); } - StorageProgram::ClaimStorageReward { entry_height } => { + StorageInstruction::ClaimStorageReward { entry_height } => { let claims_index = get_segment_from_entry(entry_height); let account_key = keyed_accounts[0].signer_key().unwrap(); let mut num_validations = 0; let mut total_validations = 0; - for validation in &storage_account_state.reward_validations[claims_index] { + for validation in &storage_contract.reward_validations[claims_index] { if *account_key == validation.id { num_validations += count_valid_proofs(&validation.proof_mask); } else { @@ -154,11 +153,8 @@ pub fn process_instruction( } } - if bincode::serialize_into( - &mut keyed_accounts[0].account.data[..], - &storage_account_state, - ) - .is_err() + if bincode::serialize_into(&mut keyed_accounts[0].account.data[..], &storage_contract) + .is_err() { return Err(InstructionError::AccountDataTooSmall); } @@ -173,193 +169,162 @@ pub fn process_instruction( #[cfg(test)] mod tests { use super::*; - use crate::{ProofStatus, StorageTransaction, ENTRIES_PER_SEGMENT}; + use crate::id; + use crate::storage_contract::ProofStatus; + use crate::ENTRIES_PER_SEGMENT; use bincode::deserialize; use solana_runtime::bank::Bank; + use solana_runtime::bank_client::BankClient; use solana_sdk::account::{create_keyed_accounts, Account}; use solana_sdk::genesis_block::GenesisBlock; use solana_sdk::hash::{hash, Hash}; + use solana_sdk::instruction::Instruction; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; - use solana_sdk::system_transaction::SystemTransaction; - use solana_sdk::transaction::{CompiledInstruction, Transaction}; + use solana_sdk::system_instruction::SystemInstruction; - fn test_transaction( - tx: &Transaction, + fn test_instruction( + ix: &Instruction, program_accounts: &mut [Account], ) -> Result<(), InstructionError> { - assert_eq!(tx.instructions.len(), 1); - let CompiledInstruction { - ref accounts, - ref data, - .. - } = tx.instructions[0]; - - info!("accounts: {:?}", accounts); - - let mut keyed_accounts: Vec<_> = accounts + let mut keyed_accounts: Vec<_> = ix + .accounts .iter() - .map(|&index| { - let index = index as usize; - let key = &tx.account_keys[index]; - (key, index < tx.signatures.len()) - }) .zip(program_accounts.iter_mut()) - .map(|((key, is_signer), account)| KeyedAccount::new(key, is_signer, account)) + .map(|(account_meta, account)| { + KeyedAccount::new(&account_meta.pubkey, account_meta.is_signer, account) + }) .collect(); - let ret = process_instruction(&id(), &mut keyed_accounts, &data, 42); + let ret = process_instruction(&id(), &mut keyed_accounts, &ix.data, 42); info!("ret: {:?}", ret); ret } #[test] fn test_storage_tx() { - let keypair = Keypair::new(); - let mut accounts = [(keypair.pubkey(), Account::default())]; + let pubkey = Keypair::new().pubkey(); + let mut accounts = [(pubkey, Account::default())]; let mut keyed_accounts = create_keyed_accounts(&mut accounts); assert!(process_instruction(&id(), &mut keyed_accounts, &[], 42).is_err()); } #[test] fn test_serialize_overflow() { - let keypair = Keypair::new(); + let pubkey = Keypair::new().pubkey(); let mut keyed_accounts = Vec::new(); let mut user_account = Account::default(); - let pubkey = keypair.pubkey(); keyed_accounts.push(KeyedAccount::new(&pubkey, true, &mut user_account)); - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), + let ix = StorageInstruction::new_advertise_recent_blockhash( + &pubkey, Hash::default(), ENTRIES_PER_SEGMENT, ); assert_eq!( - process_instruction(&id(), &mut keyed_accounts, &tx.instructions[0].data, 42), + process_instruction(&id(), &mut keyed_accounts, &ix.data, 42), Err(InstructionError::AccountDataTooSmall) ); } #[test] fn test_invalid_accounts_len() { - let keypair = Keypair::new(); + let pubkey = Keypair::new().pubkey(); let mut accounts = [Account::default()]; - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), - Hash::default(), - 0, - Signature::default(), - ); - assert!(test_transaction(&tx, &mut accounts).is_err()); + let ix = + StorageInstruction::new_mining_proof(&pubkey, Hash::default(), 0, Signature::default()); + assert!(test_instruction(&ix, &mut accounts).is_err()); let mut accounts = [Account::default(), Account::default(), Account::default()]; - assert!(test_transaction(&tx, &mut accounts).is_err()); + assert!(test_instruction(&ix, &mut accounts).is_err()); } #[test] fn test_submit_mining_invalid_entry_height() { solana_logger::setup(); - let keypair = Keypair::new(); + let pubkey = Keypair::new().pubkey(); let mut accounts = [Account::default(), Account::default()]; accounts[1].data.resize(16 * 1024, 0); - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), - Hash::default(), - 0, - Signature::default(), - ); + let ix = + StorageInstruction::new_mining_proof(&pubkey, Hash::default(), 0, Signature::default()); // Haven't seen a transaction to roll over the epoch, so this should fail - assert!(test_transaction(&tx, &mut accounts).is_err()); + assert!(test_instruction(&ix, &mut accounts).is_err()); } #[test] fn test_submit_mining_ok() { solana_logger::setup(); - let keypair = Keypair::new(); + let pubkey = Keypair::new().pubkey(); let mut accounts = [Account::default(), Account::default()]; accounts[0].data.resize(16 * 1024, 0); - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), + let ix = StorageInstruction::new_advertise_recent_blockhash( + &pubkey, Hash::default(), ENTRIES_PER_SEGMENT, ); - test_transaction(&tx, &mut accounts).unwrap(); + test_instruction(&ix, &mut accounts).unwrap(); - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), - Hash::default(), - 0, - Signature::default(), - ); + let ix = + StorageInstruction::new_mining_proof(&pubkey, Hash::default(), 0, Signature::default()); - test_transaction(&tx, &mut accounts).unwrap(); + test_instruction(&ix, &mut accounts).unwrap(); } #[test] fn test_validate_mining() { solana_logger::setup(); - let keypair = Keypair::new(); + let pubkey = Keypair::new().pubkey(); let mut accounts = [Account::default(), Account::default()]; accounts[0].data.resize(16 * 1024, 0); let entry_height = 0; - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), + let ix = StorageInstruction::new_advertise_recent_blockhash( + &pubkey, Hash::default(), ENTRIES_PER_SEGMENT, ); - test_transaction(&tx, &mut accounts).unwrap(); + test_instruction(&ix, &mut accounts).unwrap(); - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), + let ix = StorageInstruction::new_mining_proof( + &pubkey, Hash::default(), entry_height, Signature::default(), ); - test_transaction(&tx, &mut accounts).unwrap(); + test_instruction(&ix, &mut accounts).unwrap(); - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), + let ix = StorageInstruction::new_advertise_recent_blockhash( + &pubkey, Hash::default(), ENTRIES_PER_SEGMENT * 2, ); - test_transaction(&tx, &mut accounts).unwrap(); + test_instruction(&ix, &mut accounts).unwrap(); - let tx = StorageTransaction::new_proof_validation( - &keypair, - Hash::default(), + let ix = StorageInstruction::new_proof_validation( + &pubkey, entry_height, vec![ProofStatus::Valid], ); - test_transaction(&tx, &mut accounts).unwrap(); + test_instruction(&ix, &mut accounts).unwrap(); - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), + let ix = StorageInstruction::new_advertise_recent_blockhash( + &pubkey, Hash::default(), ENTRIES_PER_SEGMENT * 3, ); - test_transaction(&tx, &mut accounts).unwrap(); + test_instruction(&ix, &mut accounts).unwrap(); - let tx = StorageTransaction::new_reward_claim(&keypair, Hash::default(), entry_height); - test_transaction(&tx, &mut accounts).unwrap(); + let ix = StorageInstruction::new_reward_claim(&pubkey, entry_height); + test_instruction(&ix, &mut accounts).unwrap(); assert!(accounts[0].lamports == TOTAL_VALIDATOR_REWARDS); } @@ -367,10 +332,10 @@ mod tests { fn get_storage_entry_height(bank: &Bank, account: &Pubkey) -> u64 { match bank.get_account(&account) { Some(storage_system_account) => { - let state = deserialize(&storage_system_account.data); - if let Ok(state) = state { - let state: StorageProgramState = state; - return state.entry_height; + let contract = deserialize(&storage_system_account.data); + if let Ok(contract) = contract { + let contract: StorageContract = contract; + return contract.entry_height; } } None => { @@ -382,10 +347,10 @@ mod tests { fn get_storage_blockhash(bank: &Bank, account: &Pubkey) -> Hash { if let Some(storage_system_account) = bank.get_account(&account) { - let state = deserialize(&storage_system_account.data); - if let Ok(state) = state { - let state: StorageProgramState = state; - return state.hash; + let contract = deserialize(&storage_system_account.data); + if let Ok(contract) = contract { + let contract: StorageContract = contract; + return contract.hash; } } Hash::default() @@ -393,15 +358,18 @@ mod tests { #[test] fn test_bank_storage() { - let (mut genesis_block, alice) = GenesisBlock::new(1000); + let (mut genesis_block, mint_keypair) = GenesisBlock::new(1000); genesis_block .native_programs .push(("solana_storage_program".to_string(), id())); let bank = Bank::new(&genesis_block); - - let bob = Keypair::new(); - let jack = Keypair::new(); - let jill = Keypair::new(); + let alice_client = BankClient::new(&bank, mint_keypair); + let alice_pubkey = alice_client.pubkey(); + let bob_keypair = Keypair::new(); + let bob_client = BankClient::new(&bank, bob_keypair); + let bob_pubkey = bob_client.pubkey(); + let jack_pubkey = Keypair::new().pubkey(); + let jill_pubkey = Keypair::new().pubkey(); let x = 42; let blockhash = genesis_block.hash(); @@ -410,51 +378,36 @@ mod tests { bank.register_tick(&blockhash); - bank.transfer(10, &alice, &jill.pubkey(), blockhash) - .unwrap(); - - bank.transfer(10, &alice, &bob.pubkey(), blockhash).unwrap(); - bank.transfer(10, &alice, &jack.pubkey(), blockhash) - .unwrap(); - - let tx = SystemTransaction::new_program_account( - &alice, - &bob.pubkey(), - blockhash, - 1, - 4 * 1024, - &id(), - 0, - ); + alice_client.transfer(10, &jill_pubkey).unwrap(); + alice_client.transfer(10, &bob_pubkey).unwrap(); + alice_client.transfer(10, &jack_pubkey).unwrap(); - bank.process_transaction(&tx).unwrap(); + let ix = + SystemInstruction::new_program_account(&alice_pubkey, &bob_pubkey, 1, 4 * 1024, &id()); - let tx = StorageTransaction::new_advertise_recent_blockhash( - &bob, + alice_client.process_instruction(ix).unwrap(); + + let ix = StorageInstruction::new_advertise_recent_blockhash( + &bob_pubkey, storage_blockhash, - blockhash, ENTRIES_PER_SEGMENT, ); - bank.process_transaction(&tx).unwrap(); + bob_client.process_instruction(ix).unwrap(); let entry_height = 0; - let tx = StorageTransaction::new_mining_proof( - &bob, + let ix = StorageInstruction::new_mining_proof( + &bob_pubkey, Hash::default(), - blockhash, entry_height, Signature::default(), ); - let _result = bank.process_transaction(&tx).unwrap(); + let _result = bob_client.process_instruction(ix).unwrap(); assert_eq!( - get_storage_entry_height(&bank, &bob.pubkey()), + get_storage_entry_height(&bank, &bob_pubkey), ENTRIES_PER_SEGMENT ); - assert_eq!( - get_storage_blockhash(&bank, &bob.pubkey()), - storage_blockhash - ); + assert_eq!(get_storage_blockhash(&bank, &bob_pubkey), storage_blockhash); } } diff --git a/programs/token_api/src/token_processor.rs b/programs/token_api/src/token_processor.rs index f98b08dd7f30b5..8389d8b3b11104 100644 --- a/programs/token_api/src/token_processor.rs +++ b/programs/token_api/src/token_processor.rs @@ -2,8 +2,8 @@ use crate::token_state::TokenState; use bincode::serialize; use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::InstructionError; pub fn process_instruction( program_id: &Pubkey, diff --git a/programs/vote_api/src/vote_instruction.rs b/programs/vote_api/src/vote_instruction.rs index e7b76425227e78..5e1c23cc93996c 100644 --- a/programs/vote_api/src/vote_instruction.rs +++ b/programs/vote_api/src/vote_instruction.rs @@ -1,7 +1,7 @@ use crate::id; use serde_derive::{Deserialize, Serialize}; +use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::{AccountMeta, Instruction}; #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Vote { diff --git a/programs/vote_api/src/vote_processor.rs b/programs/vote_api/src/vote_processor.rs index ed295ec1f71ca2..ea586522b044ed 100644 --- a/programs/vote_api/src/vote_processor.rs +++ b/programs/vote_api/src/vote_processor.rs @@ -6,8 +6,8 @@ use crate::vote_state; use bincode::deserialize; use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::InstructionError; pub fn process_instruction( _program_id: &Pubkey, @@ -51,10 +51,11 @@ mod tests { use solana_runtime::bank::{Bank, Result}; use solana_runtime::bank_client::BankClient; use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError}; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::system_instruction::SystemInstruction; - use solana_sdk::transaction::{AccountMeta, Instruction, InstructionError, TransactionError}; + use solana_sdk::transaction::TransactionError; fn create_bank(lamports: u64) -> (Bank, Keypair) { let (genesis_block, mint_keypair) = GenesisBlock::new(lamports); diff --git a/programs/vote_api/src/vote_state.rs b/programs/vote_api/src/vote_state.rs index 8a95f107edcad5..b7435c83822a75 100644 --- a/programs/vote_api/src/vote_state.rs +++ b/programs/vote_api/src/vote_state.rs @@ -7,8 +7,8 @@ use bincode::{deserialize, serialize_into, serialized_size, ErrorKind}; use log::*; use serde_derive::{Deserialize, Serialize}; use solana_sdk::account::{Account, KeyedAccount}; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::InstructionError; use std::collections::VecDeque; // Maximum number of votes to keep around diff --git a/proposals/README.md b/proposals/README.md deleted file mode 100644 index eeeb4655d17311..00000000000000 --- a/proposals/README.md +++ /dev/null @@ -1,26 +0,0 @@ -Building the Solana Design Proposals HTML ---- - -Install the book's dependencies, build, and test the book: - -```bash -$ ./build.sh -``` - -Run any Rust tests in the markdown: - -```bash -$ make test -``` - -Render markdown as HTML: - -```bash -$ make build -``` - -Render and view the book: - -```bash -$ make open -``` diff --git a/proposals/book.toml b/proposals/book.toml deleted file mode 100644 index 6e0c23d233cbd6..00000000000000 --- a/proposals/book.toml +++ /dev/null @@ -1,10 +0,0 @@ -[book] -title = "Solana Design Proposals" -authors = ["The Solana Team"] - -[build] -build-dir = "html" -create-missing = false - -[output.html] -theme = "../book/theme" diff --git a/proposals/build.sh b/proposals/build.sh deleted file mode 100755 index 14063a5279c3d6..00000000000000 --- a/proposals/build.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -e - -cd "$(dirname "$0")" - -cargo_install_unless() { - declare crate=$1 - shift - - "$@" > /dev/null 2>&1 || \ - cargo install "$crate" -} - -export PATH=$CARGO_HOME/bin:$PATH -cargo_install_unless mdbook mdbook --help -cargo_install_unless svgbob_cli svgbob --help - -make -j"$(nproc)" diff --git a/proposals/makefile b/proposals/makefile deleted file mode 100644 index ab526be7a0e782..00000000000000 --- a/proposals/makefile +++ /dev/null @@ -1,38 +0,0 @@ -BOB_SRCS=$(wildcard art/*.bob) -MSC_SRCS=$(wildcard art/*.msc) -MD_SRCS=$(wildcard src/*.md) - -SVG_IMGS=$(BOB_SRCS:art/%.bob=src/img/%.svg) $(MSC_SRCS:art/%.msc=src/img/%.svg) - -all: html/index.html - -test: src/tests.ok - -open: all - mdbook build --open - -watch: $(SVG_IMGS) - mdbook watch - -src/img/%.svg: art/%.bob - @mkdir -p $(@D) - svgbob < $< > $@ - -src/img/%.svg: art/%.msc - @mkdir -p $(@D) - mscgen -T svg -i $< -o $@ - -src/%.md: %.md - @mkdir -p $(@D) - @cp $< $@ - -src/tests.ok: $(SVG_IMGS) $(MD_SRCS) - mdbook test - touch $@ - -html/index.html: src/tests.ok - mdbook build - -clean: - rm -f $(SVG_IMGS) src/tests.ok - rm -rf html diff --git a/proposals/src/SUMMARY.md b/proposals/src/SUMMARY.md deleted file mode 100644 index c9a41307cf7990..00000000000000 --- a/proposals/src/SUMMARY.md +++ /dev/null @@ -1,32 +0,0 @@ -# Solana Design Proposals - -- [Accepted Design Proposals](proposals.md) - - [Ledger Replication](ledger-replication-to-implement.md) - - [Secure Vote Signing](vote-signing-to-implement.md) - - [Staking Rewards](staking-rewards.md) - - [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md) - - [Fork Selection](fork-selection.md) - - [Reliable Vote Transmission](reliable-vote-transmission.md) - - [Persistent Account Storage](persistent-account-storage.md) - - [Credit-Only Credit-Debit Accounts](credit-only-credit-debit-accounts.md) - - [Cluster Economics](ed_overview.md) - - [Validation-client Economics](ed_validation_client_economics.md) - - [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md) - - [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) - - [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md) - - [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) - - [Replication-client Economics](ed_replication_client_economics.md) - - [Storage-replication Rewards](ed_rce_storage_replication_rewards.md) - - [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md) - - [Economic Sustainability](ed_economic_sustainability.md) - - [Attack Vectors](ed_attack_vectors.md) - - [Economic Design MVP](ed_mvp.md) - - [References](ed_references.md) - - [Cluster Test Framework](cluster-test-framework.md) - - [Testing Programs](testing-programs.md) - -- [Implemented Design Proposals](implemented-proposals.md) - - [Leader-to-Leader Transition](leader-leader-transition.md) - - [Leader-to-Validator Transition](leader-validator-transition.md) - - [Cluster Software Installation and Updates](installer.md) - - [Testnet Participation](testnet-participation.md) diff --git a/proposals/src/installer.md b/proposals/src/installer.md index a69b37e5eb89f6..571a4317cc1328 100644 --- a/proposals/src/installer.md +++ b/proposals/src/installer.md @@ -9,22 +9,34 @@ use binaries supplied by Solana or any other party they trust. Deployment of updates is managed using an on-chain update manifest program. ### Motivating Examples -#### Fetch and run a pre-built installer +#### Fetch and run a pre-built installer using a bootstrap curl/shell script +The easiest install method for supported platforms: +```bash +$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | sh +``` + +This script will check github for the latest tagged release and download and run the +`solana-install` binary from there. + + +If additional arguments need to be specified during the installation, the +following shell syntax is used: +```bash +$ init_args=.... # arguments for `solana-installer init ...` +$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | sh -s - ${init_args} +``` +#### Fetch and run a pre-built installer from a Github release With a well-known release URL, a pre-built binary can be obtained for supported platforms: ```bash -$ curl -o solana-install https://github.com/solana-labs/solana/releases/download/v1.2.3/solana-install-x86_64-apple-darwin +$ curl -o solana-install https://github.com/solana-labs/solana/releases/download/v0.13.0/solana-install-x86_64-apple-darwin $ chmod +x ./solana-install $ ./solana-install --help ``` -Note: future enhancements should include building wrapper in the style of -https://rustup.rs/ to make bootstrapping an install even easier. - #### Build and run the installer from source - If a pre-built binary is not available for a given platform, building the installer from source is always an option: ```bash @@ -42,7 +54,6 @@ $ solana-install deploy http://example.com/path/to/solana-release.tar.bz2 update ``` #### Run a validator node that auto updates itself - ```bash $ solana-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates $ export PATH=~/.local/share/solana-install/bin:$PATH @@ -99,7 +110,6 @@ bzip2 with the following internal structure: * `...` -- any additional files and directories are permitted ### solana-install Tool - The `solana-install` tool is used by the user to install and update their cluster software. It manages the following files and directories in the user's home directory: @@ -108,7 +118,6 @@ It manages the following files and directories in the user's home directory: * `~/.local/share/solana/install/releases//` - contents of a release #### Command-line Interface - ```manpage solana-install 0.13.0 The solana cluster software installer diff --git a/proposals/src/proposals.md b/proposals/src/proposals.md deleted file mode 100644 index d55dd9ae6f5ca6..00000000000000 --- a/proposals/src/proposals.md +++ /dev/null @@ -1,7 +0,0 @@ -# Proposed Architectural Changes - -The following architectural proposals have been accepted by the Solana team, -but are not yet fully implemented. The proposals may be implemented as -described, implemented differently as issues in the designs become evident, or -not implemented at all. If implemented, the they will be moved from this -section to the following one in a future version of this book. diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 47a7e40f430cb3..1c447e9c178a5d 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -991,9 +991,8 @@ mod tests { use rand::{thread_rng, Rng}; use solana_sdk::account::Account; use solana_sdk::hash::Hash; - use solana_sdk::signature::Keypair; - use solana_sdk::signature::KeypairUtil; - use solana_sdk::transaction::CompiledInstruction; + use solana_sdk::instruction::CompiledInstruction; + use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::transaction::Transaction; fn cleanup_paths(paths: &str) { diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 0848a1a8163c9e..b25b0eecf15cc8 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -261,7 +261,11 @@ impl Bank { let parent_caches: Vec<_> = parents .iter() - .map(|b| b.status_cache.read().unwrap()) + .map(|p| { + let mut parent = p.status_cache.write().unwrap(); + parent.freeze(); + parent + }) .collect(); self.status_cache.write().unwrap().squash(&parent_caches); } @@ -924,11 +928,11 @@ mod tests { use bincode::serialize; use solana_sdk::genesis_block::{GenesisBlock, BOOTSTRAP_LEADER_LAMPORTS}; use solana_sdk::hash; + use solana_sdk::instruction::{CompiledInstruction, InstructionError}; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::system_instruction::SystemInstruction; use solana_sdk::system_program; use solana_sdk::system_transaction::SystemTransaction; - use solana_sdk::transaction::{CompiledInstruction, InstructionError}; #[test] fn test_bank_new() { diff --git a/runtime/src/bank_client.rs b/runtime/src/bank_client.rs index b66f2f2782082c..2a4ef1c5f05512 100644 --- a/runtime/src/bank_client.rs +++ b/runtime/src/bank_client.rs @@ -1,9 +1,10 @@ use crate::bank::Bank; +use solana_sdk::instruction::Instruction; use solana_sdk::pubkey::Pubkey; use solana_sdk::script::Script; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::system_instruction::SystemInstruction; -use solana_sdk::transaction::{Instruction, Transaction, TransactionError}; +use solana_sdk::transaction::{Transaction, TransactionError}; pub struct BankClient<'a> { bank: &'a Bank, @@ -67,7 +68,7 @@ impl<'a> BankClient<'a> { mod tests { use super::*; use solana_sdk::genesis_block::GenesisBlock; - use solana_sdk::transaction::AccountMeta; + use solana_sdk::instruction::AccountMeta; #[test] fn test_bank_client_new_with_keypairs() { diff --git a/runtime/src/loader_utils.rs b/runtime/src/loader_utils.rs index 943e59eb198b04..b4251b986d14f6 100644 --- a/runtime/src/loader_utils.rs +++ b/runtime/src/loader_utils.rs @@ -1,11 +1,11 @@ use crate::bank::Bank; use crate::bank_client::BankClient; use serde::Serialize; +use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::loader_instruction::LoaderInstruction; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::system_instruction::SystemInstruction; -use solana_sdk::transaction::{AccountMeta, Instruction}; pub fn load_program( bank: &Bank, diff --git a/runtime/src/native_loader.rs b/runtime/src/native_loader.rs index 278a201e737fc1..90752d60a9ba97 100644 --- a/runtime/src/native_loader.rs +++ b/runtime/src/native_loader.rs @@ -6,10 +6,10 @@ use libloading::os::unix::*; use libloading::os::windows::*; use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::loader_instruction::LoaderInstruction; use solana_sdk::native_program; use solana_sdk::pubkey::Pubkey; -use solana_sdk::transaction::InstructionError; use std::env; use std::path::PathBuf; use std::str; diff --git a/runtime/src/runtime.rs b/runtime/src/runtime.rs index de9b96d38d8628..0a013dc47c3fef 100644 --- a/runtime/src/runtime.rs +++ b/runtime/src/runtime.rs @@ -1,8 +1,9 @@ use crate::native_loader; use solana_sdk::account::{create_keyed_accounts, Account, KeyedAccount}; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_program; -use solana_sdk::transaction::{InstructionError, Transaction, TransactionError}; +use solana_sdk::transaction::{Transaction, TransactionError}; /// Return true if the slice has any duplicate elements pub fn has_duplicates(xs: &[T]) -> bool { diff --git a/runtime/src/system_program.rs b/runtime/src/system_program.rs index ed24ad1ac12f0e..435d18db5568a5 100644 --- a/runtime/src/system_program.rs +++ b/runtime/src/system_program.rs @@ -1,10 +1,10 @@ use bincode::serialize; use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_instruction::{SystemError, SystemInstruction}; use solana_sdk::system_program; -use solana_sdk::transaction::InstructionError; const FROM_ACCOUNT_INDEX: usize = 0; const TO_ACCOUNT_INDEX: usize = 1; @@ -108,11 +108,12 @@ mod tests { use crate::bank_client::BankClient; use solana_sdk::account::Account; use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError}; use solana_sdk::script::Script; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::system_instruction::SystemInstruction; use solana_sdk::system_program; - use solana_sdk::transaction::{AccountMeta, Instruction, InstructionError, TransactionError}; + use solana_sdk::transaction::TransactionError; #[test] fn test_create_system_account() { diff --git a/sdk/src/instruction.rs b/sdk/src/instruction.rs new file mode 100644 index 00000000000000..8aa4e42bc651a5 --- /dev/null +++ b/sdk/src/instruction.rs @@ -0,0 +1,149 @@ +//! Defines a composable Instruction type and a memory-efficient CompiledInstruction. + +use crate::pubkey::Pubkey; +use crate::shortvec::{deserialize_vec_bytes, encode_len, serialize_vec_bytes}; +use crate::system_instruction::SystemError; +use bincode::{serialize, Error}; +use serde::Serialize; +use std::io::{Cursor, Read, Write}; +use std::mem::size_of; + +/// Reasons the runtime might have rejected an instruction. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum InstructionError { + /// Deprecated! Use CustomError instead! + /// The program instruction returned an error + GenericError, + + /// The arguments provided to a program instruction where invalid + InvalidArgument, + + /// An instruction's data contents was invalid + InvalidInstructionData, + + /// An account's data contents was invalid + InvalidAccountData, + + /// An account's data was too small + AccountDataTooSmall, + + /// The account did not have the expected program id + IncorrectProgramId, + + /// A signature was required but not found + MissingRequiredSignature, + + /// An initialize instruction was sent to an account that has already been initialized. + AccountAlreadyInitialized, + + /// An attempt to operate on an account that hasn't been initialized. + UninitializedAccount, + + /// Program's instruction lamport balance does not equal the balance after the instruction + UnbalancedInstruction, + + /// Program modified an account's program id + ModifiedProgramId, + + /// Program spent the lamports of an account that doesn't belong to it + ExternalAccountLamportSpend, + + /// Program modified the data of an account that doesn't belong to it + ExternalAccountDataModified, + + /// An account was referenced more than once in a single instruction + DuplicateAccountIndex, + + /// CustomError allows on-chain programs to implement program-specific error types and see + /// them returned by the Solana runtime. A CustomError may be any type that is serialized + /// to a Vec of bytes, max length 32 bytes. Any CustomError Vec greater than this length will + /// be truncated by the runtime. + CustomError(Vec), +} + +impl InstructionError { + pub fn new_result_with_negative_lamports() -> Self { + let serialized_error = + bincode::serialize(&SystemError::ResultWithNegativeLamports).unwrap(); + InstructionError::CustomError(serialized_error) + } +} + +/// An instruction to execute a program +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct GenericInstruction { + /// Index into the transaction program ids array indicating the program account that executes this instruction + pub program_ids_index: P, + /// Ordered indices into the transaction keys array indicating which accounts to pass to the program + pub accounts: Vec, + /// The program input data + pub data: Vec, +} + +impl GenericInstruction { + pub fn new(program_ids_index: P, data: &T, accounts: Vec) -> Self { + let data = serialize(data).unwrap(); + Self { + program_ids_index, + data, + accounts, + } + } +} + +/// Account metadata used to define Instructions +#[derive(Debug)] +pub struct AccountMeta { + /// An account's public key + pub pubkey: Pubkey, + /// True if an Instruciton requires a Transaction signature matching `pubkey`. + pub is_signer: bool, +} + +impl AccountMeta { + pub fn new(pubkey: Pubkey, is_signer: bool) -> Self { + Self { pubkey, is_signer } + } +} + +pub type Instruction = GenericInstruction; +pub type CompiledInstruction = GenericInstruction; + +impl CompiledInstruction { + pub fn serialize_with(mut writer: &mut Cursor<&mut [u8]>, ix: &Self) -> Result<(), Error> { + writer.write_all(&[ix.program_ids_index])?; + serialize_vec_bytes(&mut writer, &ix.accounts[..])?; + serialize_vec_bytes(&mut writer, &ix.data[..])?; + Ok(()) + } + + pub fn deserialize_from(mut reader: &mut Cursor<&[u8]>) -> Result { + let mut buf = [0]; + reader.read_exact(&mut buf)?; + let program_ids_index = buf[0]; + let accounts = deserialize_vec_bytes(&mut reader)?; + let data = deserialize_vec_bytes(&mut reader)?; + Ok(CompiledInstruction { + program_ids_index, + accounts, + data, + }) + } + + pub fn serialized_size(&self) -> Result { + let mut buf = [0; size_of::() + 1]; + let mut wr = Cursor::new(&mut buf[..]); + let mut size = size_of::(); + + let len = self.accounts.len(); + encode_len(&mut wr, len)?; + size += wr.position() as usize + (len * size_of::()); + + let len = self.data.len(); + wr.set_position(0); + encode_len(&mut wr, len)?; + size += wr.position() as usize + (len * size_of::()); + + Ok(size as u64) + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index bc1e642d6807c4..725213a4974b48 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -2,6 +2,7 @@ pub mod account; pub mod bpf_loader; pub mod genesis_block; pub mod hash; +pub mod instruction; pub mod loader_instruction; pub mod native_loader; pub mod native_program; diff --git a/sdk/src/loader_instruction.rs b/sdk/src/loader_instruction.rs index 09b6465f5b76dd..f020474a596ad9 100644 --- a/sdk/src/loader_instruction.rs +++ b/sdk/src/loader_instruction.rs @@ -1,5 +1,5 @@ +use crate::instruction::{AccountMeta, Instruction}; use crate::pubkey::Pubkey; -use crate::transaction::{AccountMeta, Instruction}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum LoaderInstruction { diff --git a/sdk/src/native_program.rs b/sdk/src/native_program.rs index 86c28cc918b616..88fcb4f26bbaa0 100644 --- a/sdk/src/native_program.rs +++ b/sdk/src/native_program.rs @@ -1,6 +1,6 @@ use crate::account::KeyedAccount; +use crate::instruction::InstructionError; use crate::pubkey::Pubkey; -use crate::transaction::InstructionError; // All native programs export a symbol named process() pub const ENTRYPOINT: &str = "process"; @@ -24,7 +24,7 @@ macro_rules! solana_entrypoint( keyed_accounts: &mut [solana_sdk::account::KeyedAccount], data: &[u8], tick_height: u64 - ) -> Result<(), solana_sdk::transaction::InstructionError> { + ) -> Result<(), solana_sdk::instruction::InstructionError> { $entrypoint(program_id, keyed_accounts, data, tick_height) } ) @@ -40,7 +40,7 @@ macro_rules! process_instruction_entrypoint( keyed_accounts: &mut [solana_sdk::account::KeyedAccount], data: &[u8], tick_height: u64, - ) -> Result<(), solana_sdk::transaction::InstructionError> { + ) -> Result<(), solana_sdk::instruction::InstructionError> { solana_logger::setup(); log::trace!("process_instruction: {:?}", data); diff --git a/sdk/src/script.rs b/sdk/src/script.rs index f034fbd023f496..2301ca86662097 100644 --- a/sdk/src/script.rs +++ b/sdk/src/script.rs @@ -1,8 +1,9 @@ //! A library for building scripts and compiling them into transactions. use crate::hash::Hash; +use crate::instruction::{CompiledInstruction, Instruction}; use crate::pubkey::Pubkey; -use crate::transaction::{CompiledInstruction, Instruction, Transaction}; +use crate::transaction::Transaction; use itertools::Itertools; fn position(keys: &[Pubkey], key: &Pubkey) -> u8 { @@ -104,8 +105,8 @@ impl Script { #[cfg(test)] mod tests { use super::*; + use crate::instruction::AccountMeta; use crate::signature::{Keypair, KeypairUtil}; - use crate::transaction::AccountMeta; #[test] fn test_transaction_builder_unique_program_ids() { diff --git a/sdk/src/system_instruction.rs b/sdk/src/system_instruction.rs index 5a115e2cd06e02..ea25aafc36aa84 100644 --- a/sdk/src/system_instruction.rs +++ b/sdk/src/system_instruction.rs @@ -1,6 +1,6 @@ +use crate::instruction::{AccountMeta, Instruction}; use crate::pubkey::Pubkey; use crate::system_program; -use crate::transaction::{AccountMeta, Instruction}; #[derive(Serialize, Debug, Clone, PartialEq)] pub enum SystemError { diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index 0cc11a93dd0bb9..11b86ecc43e5b2 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -1,161 +1,19 @@ -//! The `transaction` module provides functionality for creating log transactions. +//! Defines a Transaction type to package an atomic sequence of instructions. use crate::hash::{Hash, Hasher}; +use crate::instruction::{AccountMeta, CompiledInstruction, Instruction, InstructionError}; use crate::packet::PACKET_DATA_SIZE; use crate::pubkey::Pubkey; use crate::script::Script; -use crate::shortvec::{ - deserialize_vec_bytes, deserialize_vec_with, encode_len, serialize_vec_bytes, - serialize_vec_with, -}; +use crate::shortvec::{deserialize_vec_with, encode_len, serialize_vec_with}; use crate::signature::{KeypairUtil, Signature}; -use crate::system_instruction::SystemError; -use bincode::{serialize, Error}; +use bincode::Error; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use serde::{Deserialize, Serialize, Serializer}; use std::fmt; use std::io::{Cursor, Read, Write}; use std::mem::size_of; -/// Reasons the runtime might have rejected an instruction. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum InstructionError { - /// Deprecated! Use CustomError instead! - /// The program instruction returned an error - GenericError, - - /// The arguments provided to a program instruction where invalid - InvalidArgument, - - /// An instruction's data contents was invalid - InvalidInstructionData, - - /// An account's data contents was invalid - InvalidAccountData, - - /// An account's data was too small - AccountDataTooSmall, - - /// The account did not have the expected program id - IncorrectProgramId, - - /// A signature was required but not found - MissingRequiredSignature, - - /// An initialize instruction was sent to an account that has already been initialized. - AccountAlreadyInitialized, - - /// An attempt to operate on an account that hasn't been initialized. - UninitializedAccount, - - /// Program's instruction lamport balance does not equal the balance after the instruction - UnbalancedInstruction, - - /// Program modified an account's program id - ModifiedProgramId, - - /// Program spent the lamports of an account that doesn't belong to it - ExternalAccountLamportSpend, - - /// Program modified the data of an account that doesn't belong to it - ExternalAccountDataModified, - - /// An account was referenced more than once in a single instruction - DuplicateAccountIndex, - - /// CustomError allows on-chain programs to implement program-specific error types and see - /// them returned by the Solana runtime. A CustomError may be any type that is serialized - /// to a Vec of bytes, max length 32 bytes. Any CustomError Vec greater than this length will - /// be truncated by the runtime. - CustomError(Vec), -} - -impl InstructionError { - pub fn new_result_with_negative_lamports() -> Self { - let serialized_error = - bincode::serialize(&SystemError::ResultWithNegativeLamports).unwrap(); - InstructionError::CustomError(serialized_error) - } -} - -/// An instruction to execute a program -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct GenericInstruction { - /// Index into the transaction program ids array indicating the program account that executes this instruction - pub program_ids_index: P, - /// Ordered indices into the transaction keys array indicating which accounts to pass to the program - pub accounts: Vec, - /// The program input data - pub data: Vec, -} - -impl GenericInstruction { - pub fn new(program_ids_index: P, data: &T, accounts: Vec) -> Self { - let data = serialize(data).unwrap(); - Self { - program_ids_index, - data, - accounts, - } - } -} - -/// Account metadata used to define Instructions -pub struct AccountMeta { - /// An account's public key - pub pubkey: Pubkey, - /// True if an Instruciton requires a Transaction signature matching `pubkey`. - pub is_signer: bool, -} - -impl AccountMeta { - pub fn new(pubkey: Pubkey, is_signer: bool) -> Self { - Self { pubkey, is_signer } - } -} - -pub type Instruction = GenericInstruction; -pub type CompiledInstruction = GenericInstruction; - -impl CompiledInstruction { - pub fn serialize_with(mut writer: &mut Cursor<&mut [u8]>, ix: &Self) -> Result<(), Error> { - writer.write_all(&[ix.program_ids_index])?; - serialize_vec_bytes(&mut writer, &ix.accounts[..])?; - serialize_vec_bytes(&mut writer, &ix.data[..])?; - Ok(()) - } - - pub fn deserialize_from(mut reader: &mut Cursor<&[u8]>) -> Result { - let mut buf = [0]; - reader.read_exact(&mut buf)?; - let program_ids_index = buf[0]; - let accounts = deserialize_vec_bytes(&mut reader)?; - let data = deserialize_vec_bytes(&mut reader)?; - Ok(CompiledInstruction { - program_ids_index, - accounts, - data, - }) - } - - pub fn serialized_size(&self) -> Result { - let mut buf = [0; size_of::() + 1]; - let mut wr = Cursor::new(&mut buf[..]); - let mut size = size_of::(); - - let len = self.accounts.len(); - encode_len(&mut wr, len)?; - size += wr.position() as usize + (len * size_of::()); - - let len = self.data.len(); - wr.set_position(0); - encode_len(&mut wr, len)?; - size += wr.position() as usize + (len * size_of::()); - - Ok(size as u64) - } -} - /// Reasons a transaction might be rejected. #[derive(Debug, PartialEq, Eq, Clone)] pub enum TransactionError { @@ -552,7 +410,7 @@ impl<'de> Deserialize<'de> for Transaction { mod tests { use super::*; use crate::signature::Keypair; - use bincode::deserialize; + use bincode::{deserialize, serialize}; #[test] fn test_refs() {