diff --git a/core/src/local_cluster.rs b/core/src/local_cluster.rs index 3b75eb497..555d02a99 100644 --- a/core/src/local_cluster.rs +++ b/core/src/local_cluster.rs @@ -411,11 +411,11 @@ impl LocalCluster { let stake_account_pubkey = stake_account_keypair.pubkey(); let mut transaction = Transaction::new_signed_instructions( &[from_account.as_ref()], - vec![stake_instruction::create_account( + stake_instruction::create_delegate_account( &from_account.pubkey(), &stake_account_pubkey, amount, - )], + ), client.get_recent_blockhash().unwrap().0, ); diff --git a/core/src/staking_utils.rs b/core/src/staking_utils.rs index f74c5fb5d..46f1bfd1e 100644 --- a/core/src/staking_utils.rs +++ b/core/src/staking_utils.rs @@ -183,11 +183,11 @@ pub(crate) mod tests { process_instructions( bank, &[from_account], - vec![stake_instruction::create_account( + stake_instruction::create_delegate_account( &from_account.pubkey(), &stake_account_pubkey, amount, - )], + ), ); process_instructions( diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index 1c531701c..21bbb7b8d 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -10,26 +10,84 @@ use solana_sdk::system_instruction; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum StakeInstruction { + /// Initialize the stake account as a Delegate account. + /// + /// Expects 2 Accounts: + /// 0 - payer (TODO unused/remove) + /// 1 - Delegate StakeAccount to be initialized + InitializeDelegate, + + // Initialize the stake account as a MiningPool account + /// + /// Expects 2 Accounts: + /// 0 - payer (TODO unused/remove) + /// 1 - MiningPool StakeAccount to be initialized + InitializeMiningPool, + /// `Delegate` or `Assign` a stake account to a particular node - /// expects 2 KeyedAccounts: - /// StakeAccount to be updated - /// VoteAccount to which this Stake will be delegated + /// + /// Expects 3 Accounts: + /// 0 - payer (TODO unused/remove) + /// 1 - Delegate StakeAccount to be updated + /// 2 - VoteAccount to which this Stake will be delegated DelegateStake, /// Redeem credits in the stake account - /// expects 3 KeyedAccounts: the StakeAccount to be updated - /// and the VoteAccount to which this Stake will be delegated + /// + /// Expects 4 Accounts: + /// 0 - payer (TODO unused/remove) + /// 1 - MiningPool Stake Account to redeem credits from + /// 2 - Delegate StakeAccount to be updated + /// 3 - VoteAccount to which the Stake is delegated RedeemVoteCredits, } -pub fn create_account(from_id: &Pubkey, staker_id: &Pubkey, lamports: u64) -> Instruction { - system_instruction::create_account( - from_id, - staker_id, - lamports, - std::mem::size_of::() as u64, - &id(), - ) +pub fn create_delegate_account( + from_id: &Pubkey, + staker_id: &Pubkey, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account( + from_id, + staker_id, + lamports, + std::mem::size_of::() as u64, + &id(), + ), + Instruction::new( + id(), + &StakeInstruction::InitializeDelegate, + vec![ + AccountMeta::new(*from_id, true), + AccountMeta::new(*staker_id, false), + ], + ), + ] +} + +pub fn create_mining_pool_account( + from_id: &Pubkey, + staker_id: &Pubkey, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account( + from_id, + staker_id, + lamports, + std::mem::size_of::() as u64, + &id(), + ), + Instruction::new( + id(), + &StakeInstruction::InitializeMiningPool, + vec![ + AccountMeta::new(*from_id, true), + AccountMeta::new(*staker_id, false), + ], + ), + ] } pub fn redeem_vote_credits( @@ -67,17 +125,29 @@ pub fn process_instruction( trace!("process_instruction: {:?}", data); trace!("keyed_accounts: {:?}", keyed_accounts); - if keyed_accounts.len() < 3 { + if keyed_accounts.len() < 2 { Err(InstructionError::InvalidInstructionData)?; } - // 0th index is the guy who paid for the transaction + // 0th index is the account who paid for the transaction + // TODO: Remove the 0th index from the instruction. The stake program doesn't care who paid. let (me, rest) = &mut keyed_accounts.split_at_mut(2); - let me = &mut me[1]; // TODO: data-driven unpack and dispatch of KeyedAccounts match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? { + StakeInstruction::InitializeMiningPool => { + if !rest.is_empty() { + Err(InstructionError::InvalidInstructionData)?; + } + me.initialize_mining_pool() + } + StakeInstruction::InitializeDelegate => { + if !rest.is_empty() { + Err(InstructionError::InvalidInstructionData)?; + } + me.initialize_delegate() + } StakeInstruction::DelegateStake => { if rest.len() != 1 { Err(InstructionError::InvalidInstructionData)?; @@ -127,10 +197,6 @@ mod tests { #[test] fn test_stake_process_instruction() { - assert_eq!( - process_instruction(&create_account(&Pubkey::default(), &Pubkey::default(), 0)), - Err(InstructionError::InvalidInstructionData) // won't even decode ;) - ); assert_eq!( process_instruction(&redeem_vote_credits( &Pubkey::default(), diff --git a/programs/stake_api/src/stake_state.rs b/programs/stake_api/src/stake_state.rs index 5a256bca1..a8dff7630 100644 --- a/programs/stake_api/src/stake_state.rs +++ b/programs/stake_api/src/stake_state.rs @@ -13,6 +13,7 @@ use solana_vote_api::vote_state::VoteState; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub enum StakeState { + Uninitialized, Delegate { voter_id: Pubkey, credits_observed: u64, @@ -22,10 +23,7 @@ pub enum StakeState { impl Default for StakeState { fn default() -> Self { - StakeState::Delegate { - voter_id: Pubkey::default(), - credits_observed: 0, - } + StakeState::Uninitialized } } // TODO: trusted values of network parameters come from where? @@ -90,6 +88,8 @@ impl StakeState { } pub trait StakeAccount { + fn initialize_mining_pool(&mut self) -> Result<(), InstructionError>; + fn initialize_delegate(&mut self) -> Result<(), InstructionError>; fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError>; fn redeem_vote_credits( &mut self, @@ -99,6 +99,23 @@ pub trait StakeAccount { } impl<'a> StakeAccount for KeyedAccount<'a> { + fn initialize_mining_pool(&mut self) -> Result<(), InstructionError> { + if let StakeState::Uninitialized = self.state()? { + self.set_state(&StakeState::MiningPool) + } else { + Err(InstructionError::InvalidAccountData) + } + } + fn initialize_delegate(&mut self) -> Result<(), InstructionError> { + if let StakeState::Uninitialized = self.state()? { + self.set_state(&StakeState::Delegate { + voter_id: Pubkey::default(), + credits_observed: 0, + }) + } else { + Err(InstructionError::InvalidAccountData) + } + } fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError> { if self.signer_key().is_none() { return Err(InstructionError::MissingRequiredSignature); @@ -156,7 +173,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { }) } else { // not worth collecting - Ok(()) + Err(InstructionError::CustomError(1)) } } else { Err(InstructionError::InvalidAccountData) @@ -215,6 +232,7 @@ mod tests { assert_eq!(stake_state, StakeState::default()); } + stake_keyed_account.initialize_delegate().unwrap(); assert_eq!( stake_keyed_account.delegate_stake(&vote_keyed_account), Err(InstructionError::MissingRequiredSignature) @@ -316,6 +334,7 @@ mod tests { &id(), ); let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account); + stake_keyed_account.initialize_delegate().unwrap(); // delegate the stake assert!(stake_keyed_account @@ -338,9 +357,11 @@ mod tests { .unwrap(); // no movement in vote account, so no redemption needed - assert!(mining_pool_keyed_account - .redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account) - .is_ok()); + assert_eq!( + mining_pool_keyed_account + .redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account), + Err(InstructionError::CustomError(1)) + ); // move the vote account forward vote_state.process_vote(&Vote::new(1000)); @@ -383,6 +404,7 @@ mod tests { let pubkey = Pubkey::default(); let mut stake_account = Account::new(0, std::mem::size_of::(), &id()); let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account); + stake_keyed_account.initialize_delegate().unwrap(); // delegate the stake assert!(stake_keyed_account diff --git a/wallet/src/wallet.rs b/wallet/src/wallet.rs index d3b9217d7..de2c5f609 100644 --- a/wallet/src/wallet.rs +++ b/wallet/src/wallet.rs @@ -48,7 +48,9 @@ pub enum WalletCommand { CreateVoteAccount(Pubkey, Pubkey, u32, u64), ShowVoteAccount(Pubkey), CreateStakeAccount(Pubkey, u64), + CreateMiningPoolAccount(Pubkey, u64), DelegateStake(Keypair, Pubkey), + RedeemVoteCredits(Pubkey, Pubkey, Pubkey), ShowStakeAccount(Pubkey), Deploy(String), GetTransactionCount, @@ -216,6 +218,14 @@ pub fn parse_command( lamports, )) } + ("create-mining-pool-account", Some(matches)) => { + let mining_pool_account_id = pubkey_of(matches, "mining_pool_account_id").unwrap(); + let lamports = matches.value_of("lamports").unwrap().parse()?; + Ok(WalletCommand::CreateMiningPoolAccount( + mining_pool_account_id, + lamports, + )) + } ("delegate-stake", Some(matches)) => { let staking_account_keypair = keypair_of(matches, "staking_account_keypair_file").unwrap(); @@ -225,6 +235,16 @@ pub fn parse_command( voting_account_id, )) } + ("redeem-vote-credits", Some(matches)) => { + let mining_pool_account_id = pubkey_of(matches, "mining_pool_account_id").unwrap(); + let staking_account_id = pubkey_of(matches, "staking_account_id").unwrap(); + let voting_account_id = pubkey_of(matches, "voting_account_id").unwrap(); + Ok(WalletCommand::RedeemVoteCredits( + mining_pool_account_id, + staking_account_id, + voting_account_id, + )) + } ("show-stake-account", Some(matches)) => { let staking_account_id = pubkey_of(matches, "staking_account_id").unwrap(); Ok(WalletCommand::ShowStakeAccount(staking_account_id)) @@ -469,11 +489,28 @@ fn process_create_stake_account( lamports: u64, ) -> ProcessResult { let (recent_blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; - let ixs = vec![stake_instruction::create_account( + let ixs = stake_instruction::create_delegate_account( &config.keypair.pubkey(), staking_account_id, lamports, - )]; + ); + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + let signature_str = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair])?; + Ok(signature_str.to_string()) +} + +fn process_create_mining_pool_account( + rpc_client: &RpcClient, + config: &WalletConfig, + mining_pool_account_id: &Pubkey, + lamports: u64, +) -> ProcessResult { + let (recent_blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; + let ixs = stake_instruction::create_mining_pool_account( + &config.keypair.pubkey(), + mining_pool_account_id, + lamports, + ); let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); let signature_str = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair])?; Ok(signature_str.to_string()) @@ -501,6 +538,25 @@ fn process_delegate_stake( Ok(signature_str.to_string()) } +fn process_redeem_vote_credits( + rpc_client: &RpcClient, + config: &WalletConfig, + mining_pool_account_id: &Pubkey, + staking_account_id: &Pubkey, + voting_account_id: &Pubkey, +) -> ProcessResult { + let (recent_blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; + let ixs = vec![stake_instruction::redeem_vote_credits( + &config.keypair.pubkey(), + mining_pool_account_id, + staking_account_id, + voting_account_id, + )]; + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + let signature_str = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair])?; + Ok(signature_str.to_string()) +} + fn process_show_stake_account( rpc_client: &RpcClient, _config: &WalletConfig, @@ -518,6 +574,10 @@ fn process_show_stake_account( println!("credits observed: {}", credits_observed); Ok("".to_string()) } + Ok(StakeState::MiningPool) => { + println!("account lamports: {}", stake_account.lamports); + Ok("".to_string()) + } _ => Err(WalletError::RpcRequestError( "Account data could not be deserialized to stake state".to_string(), ))?, @@ -813,7 +873,15 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { process_create_stake_account(&rpc_client, config, &staking_account_id, *lamports) } - // Create stake account + WalletCommand::CreateMiningPoolAccount(mining_pool_account_id, lamports) => { + process_create_mining_pool_account( + &rpc_client, + config, + &mining_pool_account_id, + *lamports, + ) + } + WalletCommand::DelegateStake(staking_account_keypair, voting_account_id) => { process_delegate_stake( &rpc_client, @@ -822,6 +890,19 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { &voting_account_id, ) } + + WalletCommand::RedeemVoteCredits( + mining_pool_account_id, + staking_account_id, + voting_account_id, + ) => process_redeem_vote_credits( + &rpc_client, + config, + &mining_pool_account_id, + &staking_account_id, + &voting_account_id, + ), + // Show a vote account WalletCommand::ShowStakeAccount(staking_account_id) => { process_show_stake_account(&rpc_client, config, &staking_account_id) @@ -1099,6 +1180,27 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' ) ) .subcommand( + SubCommand::with_name("create-mining-pool-account") + .about("Create mining pool account") + .arg( + Arg::with_name("mining_pool_account_id") + .index(1) + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey) + .help("Staking account address to fund"), + ) + .arg( + Arg::with_name("lamports") + .index(2) + .value_name("NUM") + .takes_value(true) + .required(true) + .help("The number of lamports to assign to the mining pool account"), + ), + ) + .subcommand( SubCommand::with_name("create-stake-account") .about("Create staking account") .arg( @@ -1141,6 +1243,37 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' ), ) .subcommand( + SubCommand::with_name("redeem-vote-credits") + .about("Redeem credits in the staking account") + .arg( + Arg::with_name("mining_pool_account_id") + .index(1) + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey) + .help("Mining pool account to redeem credits from"), + ) + .arg( + Arg::with_name("staking_account_id") + .index(2) + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey) + .help("Staking account address to redeem credits for"), + ) + .arg( + Arg::with_name("voting_account_id") + .index(3) + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey) + .help("The voting account to which the stake was previously delegated."), + ), + ) + .subcommand( SubCommand::with_name("show-stake-account") .about("Show the contents of a stake account") .arg(