From 5f58e9cd6ee19fbc919971970901fd8d8a09bffa Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Thu, 14 Mar 2019 07:55:41 -0700 Subject: [PATCH] Config program - useful for storing/updating simple config items on chain --- Cargo.lock | 27 +++ Cargo.toml | 2 + ci/publish-crate.sh | 4 +- genesis/Cargo.toml | 1 + genesis/src/main.rs | 7 + programs/config/Cargo.toml | 28 +++ programs/config/src/lib.rs | 202 ++++++++++++++++++ programs/config_api/Cargo.toml | 21 ++ programs/config_api/src/config_instruction.rs | 29 +++ programs/config_api/src/config_transaction.rs | 45 ++++ programs/config_api/src/lib.rs | 26 +++ 11 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 programs/config/Cargo.toml create mode 100644 programs/config/src/lib.rs create mode 100644 programs/config_api/Cargo.toml create mode 100644 programs/config_api/src/config_instruction.rs create mode 100644 programs/config_api/src/config_transaction.rs create mode 100644 programs/config_api/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 625642546..59e0a3fa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2141,6 +2141,32 @@ dependencies = [ "solana-sdk 0.13.0", ] +[[package]] +name = "solana-config-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-sdk 0.13.0", +] + +[[package]] +name = "solana-config-program" +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-config-api 0.13.0", + "solana-logger 0.13.0", + "solana-metrics 0.13.0", + "solana-runtime 0.13.0", + "solana-sdk 0.13.0", +] + [[package]] name = "solana-drone" version = "0.13.0" @@ -2195,6 +2221,7 @@ dependencies = [ "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "solana 0.13.0", "solana-budget-api 0.13.0", + "solana-config-api 0.13.0", "solana-sdk 0.13.0", "solana-storage-api 0.13.0", "solana-token-api 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index e986f2fb7..afebefe1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,8 @@ members = [ "programs/bpf_loader", "programs/budget", "programs/budget_api", + "programs/config", + "programs/config_api", "programs/token", "programs/token_api", "programs/failure", diff --git a/ci/publish-crate.sh b/ci/publish-crate.sh index efa9f0a7f..cc3bbea57 100755 --- a/ci/publish-crate.sh +++ b/ci/publish-crate.sh @@ -18,9 +18,9 @@ CRATES=( metrics client drone - programs/{budget_api,rewards_api,storage_api,token_api,vote_api} + programs/{budget_api,config_api,rewards_api,storage_api,token_api,vote_api} runtime - programs/{budget,bpf_loader,vote,rewards,storage,token,vote} + programs/{budget,bpf_loader,config,vote,rewards,storage,token,vote} vote-signer core fullnode diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 06e27fbfd..542cd1a35 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -16,6 +16,7 @@ solana-sdk = { path = "../sdk", version = "0.13.0" } 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" } [dev-dependencies] hashbrown = "0.1.8" diff --git a/genesis/src/main.rs b/genesis/src/main.rs index cde41d991..c2ed7e134 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -78,6 +78,7 @@ fn main() -> Result<(), Box> { solana_storage_api::id(), ), ("solana_token_program".to_string(), solana_token_api::id()), + ("solana_config_program".to_string(), solana_config_api::id()), ]); create_new_ledger(ledger_path, &genesis_block)?; @@ -119,6 +120,10 @@ mod tests { 132, 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 config = Pubkey::new(&[ + 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, + ]); assert_eq!(solana_sdk::system_program::id(), system); assert_eq!(solana_sdk::native_loader::id(), native); @@ -127,6 +132,7 @@ mod tests { assert_eq!(solana_storage_api::id(), storage); assert_eq!(solana_token_api::id(), token); assert_eq!(solana_vote_api::id(), vote); + assert_eq!(solana_config_api::id(), config); } #[test] @@ -140,6 +146,7 @@ mod tests { solana_storage_api::id(), solana_token_api::id(), solana_vote_api::id(), + solana_config_api::id(), ]; assert!(ids.into_iter().all(move |id| unique.insert(id))); } diff --git a/programs/config/Cargo.toml b/programs/config/Cargo.toml new file mode 100644 index 000000000..588c784fb --- /dev/null +++ b/programs/config/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "solana-config-program" +version = "0.13.0" +description = "config 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-logger = { path = "../../logger", version = "0.13.0" } +solana-metrics = { path = "../../metrics", version = "0.13.0" } +solana-sdk = { path = "../../sdk", version = "0.13.0" } +solana-config-api = { path = "../config_api", version = "0.13.0" } + +[dev-dependencies] +bincode = "1.1.2" +serde = "1.0.89" +serde_derive = "1.0.89" +solana-logger = { path = "../../logger", version = "0.13.0" } +solana-runtime = { path = "../../runtime", version = "0.13.0" } + +[lib] +name = "solana_config_program" +crate-type = ["cdylib"] + diff --git a/programs/config/src/lib.rs b/programs/config/src/lib.rs new file mode 100644 index 000000000..f6664a9e9 --- /dev/null +++ b/programs/config/src/lib.rs @@ -0,0 +1,202 @@ +//! Config program + +use log::*; +use solana_config_api::check_id; +use solana_sdk::account::KeyedAccount; +use solana_sdk::native_program::ProgramError; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::solana_entrypoint; + +fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], +) -> Result<(), ProgramError> { + if !check_id(&keyed_accounts[0].account.owner) { + error!("account[0] is not assigned to the config program"); + Err(ProgramError::IncorrectProgramId)?; + } + + if keyed_accounts[0].signer_key().is_none() { + error!("account[0] should sign the transaction"); + Err(ProgramError::MissingRequiredSignature)?; + } + + if keyed_accounts[0].account.data.len() < data.len() { + error!("instruction data too large"); + Err(ProgramError::InvalidInstructionData)?; + } + + keyed_accounts[0].account.data[0..data.len()].copy_from_slice(data); + Ok(()) +} + +solana_entrypoint!(entrypoint); +fn entrypoint( + program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], + _tick_height: u64, +) -> Result<(), ProgramError> { + solana_logger::setup(); + + trace!("process_instruction: {:?}", data); + trace!("keyed_accounts: {:?}", keyed_accounts); + process_instruction(program_id, keyed_accounts, data) +} + +#[cfg(test)] +mod tests { + use super::*; + use bincode::{deserialize, serialized_size}; + use serde_derive::{Deserialize, Serialize}; + use solana_config_api::{id, ConfigInstruction, ConfigState, ConfigTransaction}; + use solana_runtime::runtime; + use solana_sdk::account::Account; + use solana_sdk::hash::Hash; + use solana_sdk::signature::{Keypair, KeypairUtil}; + use solana_sdk::system_instruction::SystemInstruction; + use solana_sdk::system_program; + use solana_sdk::transaction::Transaction; + + #[derive(Serialize, Deserialize, Default, Debug, PartialEq)] + struct MyConfig { + pub item: u64, + } + impl MyConfig { + pub fn new(item: u64) -> Self { + Self { item } + } + pub fn deserialize(input: &[u8]) -> Option { + deserialize(input).ok() + } + } + + impl ConfigState for MyConfig { + fn max_space() -> u64 { + serialized_size(&Self::default()).unwrap() + } + } + + fn create_config_account() -> Account { + Account::new(1, MyConfig::max_space() as usize, &id()) + } + + fn process_transaction( + tx: &Transaction, + tx_accounts: &mut Vec, + ) -> Result<(), ProgramError> { + runtime::process_transaction(tx, tx_accounts, process_instruction) + } + + #[test] + fn test_process_create_ok() { + solana_logger::setup(); + let from_account_keypair = Keypair::new(); + let from_account = Account::new(1, 0, &system_program::id()); + + let config_account_keypair = Keypair::new(); + let config_account = Account::new(0, 0, &system_program::id()); + + let transaction = ConfigTransaction::new_account::( + &from_account_keypair, + &config_account_keypair.pubkey(), + Hash::default(), + 1, + 0, + ); + let mut accounts = vec![from_account, config_account]; + process_transaction(&transaction, &mut accounts).unwrap(); + + assert_eq!(id(), accounts[1].owner); + assert_eq!( + MyConfig::default(), + MyConfig::deserialize(&accounts[1].data).unwrap() + ); + } + + #[test] + fn test_process_store_ok() { + solana_logger::setup(); + let config_account_keypair = Keypair::new(); + let config_account = create_config_account(); + + let new_config_state = MyConfig::new(42); + + let transaction = ConfigTransaction::new_store( + &config_account_keypair, + &new_config_state, + Hash::default(), + 0, + ); + + let mut accounts = vec![config_account]; + process_transaction(&transaction, &mut accounts).unwrap(); + + assert_eq!( + new_config_state, + MyConfig::deserialize(&accounts[0].data).unwrap() + ); + } + + #[test] + fn test_process_store_fail_instruction_data_too_large() { + solana_logger::setup(); + let config_account_keypair = Keypair::new(); + let config_account = create_config_account(); + + let new_config_state = MyConfig::new(42); + + let mut transaction = ConfigTransaction::new_store( + &config_account_keypair, + &new_config_state, + Hash::default(), + 0, + ); + + // Replace instruction data with a vector that's too large + transaction.instructions[0].data = vec![0; 123]; + + let mut accounts = vec![config_account]; + process_transaction(&transaction, &mut accounts).unwrap_err(); + } + + #[test] + fn test_process_store_fail_account0_invalid_owner() { + solana_logger::setup(); + let config_account_keypair = Keypair::new(); + let mut config_account = create_config_account(); + config_account.owner = Pubkey::default(); // <-- Invalid owner + + let new_config_state = MyConfig::new(42); + + let transaction = ConfigTransaction::new_store( + &config_account_keypair, + &new_config_state, + Hash::default(), + 0, + ); + let mut accounts = vec![config_account]; + process_transaction(&transaction, &mut accounts).unwrap_err(); + } + + #[test] + fn test_process_store_fail_account0_not_signer() { + solana_logger::setup(); + let system_account_keypair = Keypair::new(); + let system_account = Account::new(42, 0, &system_program::id()); + + let config_account_keypair = Keypair::new(); + let config_account = create_config_account(); + + let mut transaction = Transaction::new(vec![ + SystemInstruction::new_move(&system_account_keypair.pubkey(), &Pubkey::default(), 42), + ConfigInstruction::new_store(&config_account_keypair.pubkey(), &MyConfig::new(42)), + ]); + + // Don't sign the transaction with `config_account_keypair` + transaction.sign_unchecked(&[&system_account_keypair], Hash::default()); + let mut accounts = vec![system_account, config_account]; + process_transaction(&transaction, &mut accounts).unwrap_err(); + } +} diff --git a/programs/config_api/Cargo.toml b/programs/config_api/Cargo.toml new file mode 100644 index 000000000..7c468de39 --- /dev/null +++ b/programs/config_api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "solana-config-api" +version = "0.13.0" +description = "config program API" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +bincode = "1.1.2" +log = "0.4.2" +serde = "1.0.89" +serde_derive = "1.0.89" +solana-sdk = { path = "../../sdk", version = "0.13.0" } + +[lib] +name = "solana_config_api" +crate-type = ["lib"] + diff --git a/programs/config_api/src/config_instruction.rs b/programs/config_api/src/config_instruction.rs new file mode 100644 index 000000000..538162ceb --- /dev/null +++ b/programs/config_api/src/config_instruction.rs @@ -0,0 +1,29 @@ +use crate::id; +use crate::ConfigState; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::system_instruction::SystemInstruction; +use solana_sdk::transaction::Instruction; + +pub struct ConfigInstruction {} + +impl ConfigInstruction { + /// Create a new, empty configuration account + pub fn new_account( + from_account_pubkey: &Pubkey, + config_account_pubkey: &Pubkey, + lamports: u64, + ) -> Instruction { + SystemInstruction::new_program_account( + from_account_pubkey, + config_account_pubkey, + lamports, + T::max_space(), + &id(), + ) + } + + /// Store new data in a configuration account + pub fn new_store(config_account_pubkey: &Pubkey, data: &T) -> Instruction { + Instruction::new(id(), data, vec![(*config_account_pubkey, true)]) + } +} diff --git a/programs/config_api/src/config_transaction.rs b/programs/config_api/src/config_transaction.rs new file mode 100644 index 000000000..49ed8798f --- /dev/null +++ b/programs/config_api/src/config_transaction.rs @@ -0,0 +1,45 @@ +use crate::config_instruction::ConfigInstruction; +use crate::ConfigState; +use solana_sdk::hash::Hash; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::{Keypair, KeypairUtil}; +use solana_sdk::transaction::Transaction; + +pub struct ConfigTransaction {} + +impl ConfigTransaction { + /// Create a new, empty configuration account + pub fn new_account( + from_keypair: &Keypair, + config_account_pubkey: &Pubkey, + recent_blockhash: Hash, + lamports: u64, + fee: u64, + ) -> Transaction { + let mut transaction = Transaction::new(vec![ConfigInstruction::new_account::( + &from_keypair.pubkey(), + config_account_pubkey, + lamports, + )]); + transaction.fee = fee; + + transaction.sign(&[from_keypair], recent_blockhash); + transaction + } + + /// Store new state in a configuration account + pub fn new_store( + config_account_keypair: &Keypair, + data: &T, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let mut transaction = Transaction::new(vec![ConfigInstruction::new_store( + &config_account_keypair.pubkey(), + data, + )]); + transaction.fee = fee; + transaction.sign(&[config_account_keypair], recent_blockhash); + transaction + } +} diff --git a/programs/config_api/src/lib.rs b/programs/config_api/src/lib.rs new file mode 100644 index 000000000..2bdf819e9 --- /dev/null +++ b/programs/config_api/src/lib.rs @@ -0,0 +1,26 @@ +use serde::Serialize; +use solana_sdk::pubkey::Pubkey; + +mod config_instruction; +mod config_transaction; + +pub use config_instruction::ConfigInstruction; +pub use config_transaction::ConfigTransaction; + +const CONFIG_PROGRAM_ID: [u8; 32] = [ + 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, +]; + +pub fn check_id(program_id: &Pubkey) -> bool { + program_id.as_ref() == CONFIG_PROGRAM_ID +} + +pub fn id() -> Pubkey { + Pubkey::new(&CONFIG_PROGRAM_ID) +} + +pub trait ConfigState: Serialize { + /// Maximum space that the serialized representation will require + fn max_space() -> u64; +}