Add vote/stake checked instructions (backport #18345) (#18456)

* Add vote/stake checked instructions

(cherry picked from commit ee219ffa47)

# Conflicts:
#	programs/stake/src/stake_instruction.rs
#	sdk/program/src/stake/instruction.rs
#	sdk/src/feature_set.rs

* Fix set-lockup custodian index

(cherry picked from commit 544f62c92f)

* Add parsing for new stake instructions; clean up confusing test args

(cherry picked from commit 9b302ac0b5)

# Conflicts:
#	transaction-status/src/parse_stake.rs

* Add parsing for new vote instructions

(cherry picked from commit 39bac256ab)

* Add VoteInstruction::AuthorizeChecked test

(cherry picked from commit b8ca2250fd)

* Add Stake checked tests

(cherry picked from commit 74e89a3e3e)

# Conflicts:
#	programs/stake/src/stake_instruction.rs

* Fix conflicts and accommodate old apis in backport

Co-authored-by: Michael Vines <mvines@gmail.com>
Co-authored-by: Tyera Eulberg <tyera@solana.com>
This commit is contained in:
mergify[bot]
2021-07-07 03:14:54 +00:00
committed by GitHub
parent d8a2de1fd9
commit 9a7ea1229b
5 changed files with 1067 additions and 23 deletions

View File

@ -169,6 +169,61 @@ pub enum StakeInstruction {
/// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
/// lockup expiration
AuthorizeWithSeed(AuthorizeWithSeedArgs),
/// Initialize a stake with authorization information
///
/// This instruction is similar to `Initialize` except that the withdraw authority
/// must be a signer, and no lockup is applied to the account.
///
/// # Account references
/// 0. [WRITE] Uninitialized stake account
/// 1. [] Rent sysvar
/// 2. [] The stake authority
/// 3. [SIGNER] The withdraw authority
///
InitializeChecked,
/// Authorize a key to manage stake or withdrawal
///
/// This instruction behaves like `Authorize` with the additional requirement that the new
/// stake or withdraw authority must also be a signer.
///
/// # Account references
/// 0. [WRITE] Stake account to be updated
/// 1. [] Clock sysvar
/// 2. [SIGNER] The stake or withdraw authority
/// 3. [SIGNER] The new stake or withdraw authority
/// 4. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
/// lockup expiration
AuthorizeChecked(StakeAuthorize),
/// Authorize a key to manage stake or withdrawal with a derived key
///
/// This instruction behaves like `AuthorizeWithSeed` with the additional requirement that
/// the new stake or withdraw authority must also be a signer.
///
/// # Account references
/// 0. [WRITE] Stake account to be updated
/// 1. [SIGNER] Base key of stake or withdraw authority
/// 2. [] Clock sysvar
/// 3. [SIGNER] The new stake or withdraw authority
/// 4. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
/// lockup expiration
AuthorizeCheckedWithSeed(AuthorizeCheckedWithSeedArgs),
/// Set stake lockup
///
/// This instruction behaves like `SetLockup` with the additional requirement that
/// the new lockup authority also be a signer.
///
/// If a lockup is not active, the withdraw authority may set a new lockup
/// If a lockup is active, the lockup custodian may update the lockup parameters
///
/// # Account references
/// 0. [WRITE] Initialized stake account
/// 1. [SIGNER] Lockup authority or withdraw authority
/// 2. Optional: [SIGNER] New lockup authority
SetLockupChecked(LockupCheckedArgs),
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
@ -178,6 +233,12 @@ pub struct LockupArgs {
pub custodian: Option<Pubkey>,
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
pub struct LockupCheckedArgs {
pub unix_timestamp: Option<UnixTimestamp>,
pub epoch: Option<Epoch>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct AuthorizeWithSeedArgs {
pub new_authorized_pubkey: Pubkey,
@ -186,6 +247,13 @@ pub struct AuthorizeWithSeedArgs {
pub authority_owner: Pubkey,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct AuthorizeCheckedWithSeedArgs {
pub stake_authorize: StakeAuthorize,
pub authority_seed: String,
pub authority_owner: Pubkey,
}
pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction {
Instruction::new_with_bincode(
id(),
@ -197,6 +265,19 @@ pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Locku
)
}
pub fn initialize_checked(stake_pubkey: &Pubkey, authorized: &Authorized) -> Instruction {
Instruction::new_with_bincode(
id(),
&StakeInstruction::InitializeChecked,
vec![
AccountMeta::new(*stake_pubkey, false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(authorized.staker, false),
AccountMeta::new_readonly(authorized.withdrawer, true),
],
)
}
pub fn create_account_with_seed(
from_pubkey: &Pubkey,
stake_pubkey: &Pubkey,
@ -220,6 +301,46 @@ pub fn create_account_with_seed(
]
}
pub fn create_account_with_seed_checked(
from_pubkey: &Pubkey,
stake_pubkey: &Pubkey,
base: &Pubkey,
seed: &str,
authorized: &Authorized,
lamports: u64,
) -> Vec<Instruction> {
vec![
system_instruction::create_account_with_seed(
from_pubkey,
stake_pubkey,
base,
seed,
lamports,
std::mem::size_of::<StakeState>() as u64,
&id(),
),
initialize_checked(stake_pubkey, authorized),
]
}
pub fn create_account_checked(
from_pubkey: &Pubkey,
stake_pubkey: &Pubkey,
authorized: &Authorized,
lamports: u64,
) -> Vec<Instruction> {
vec![
system_instruction::create_account(
from_pubkey,
stake_pubkey,
lamports,
std::mem::size_of::<StakeState>() as u64,
&id(),
),
initialize_checked(stake_pubkey, authorized),
]
}
pub fn create_account(
from_pubkey: &Pubkey,
stake_pubkey: &Pubkey,
@ -385,6 +506,31 @@ pub fn authorize(
)
}
pub fn authorize_checked(
stake_pubkey: &Pubkey,
authorized_pubkey: &Pubkey,
new_authorized_pubkey: &Pubkey,
stake_authorize: StakeAuthorize,
custodian_pubkey: Option<&Pubkey>,
) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(*stake_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(*authorized_pubkey, true),
AccountMeta::new_readonly(*new_authorized_pubkey, true),
];
if let Some(custodian_pubkey) = custodian_pubkey {
account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true));
}
Instruction::new_with_bincode(
id(),
&StakeInstruction::AuthorizeChecked(stake_authorize),
account_metas,
)
}
pub fn authorize_with_seed(
stake_pubkey: &Pubkey,
authority_base: &Pubkey,
@ -418,6 +564,39 @@ pub fn authorize_with_seed(
)
}
pub fn authorize_checked_with_seed(
stake_pubkey: &Pubkey,
authority_base: &Pubkey,
authority_seed: String,
authority_owner: &Pubkey,
new_authorized_pubkey: &Pubkey,
stake_authorize: StakeAuthorize,
custodian_pubkey: Option<&Pubkey>,
) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(*stake_pubkey, false),
AccountMeta::new_readonly(*authority_base, true),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(*new_authorized_pubkey, true),
];
if let Some(custodian_pubkey) = custodian_pubkey {
account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true));
}
let args = AuthorizeCheckedWithSeedArgs {
stake_authorize,
authority_seed,
authority_owner: *authority_owner,
};
Instruction::new_with_bincode(
id(),
&StakeInstruction::AuthorizeCheckedWithSeed(args),
account_metas,
)
}
pub fn delegate_stake(
stake_pubkey: &Pubkey,
authorized_pubkey: &Pubkey,
@ -477,6 +656,30 @@ pub fn set_lockup(
Instruction::new_with_bincode(id(), &StakeInstruction::SetLockup(*lockup), account_metas)
}
pub fn set_lockup_checked(
stake_pubkey: &Pubkey,
lockup: &LockupArgs,
custodian_pubkey: &Pubkey,
) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(*stake_pubkey, false),
AccountMeta::new_readonly(*custodian_pubkey, true),
];
let lockup_checked = LockupCheckedArgs {
unix_timestamp: lockup.unix_timestamp,
epoch: lockup.epoch,
};
if let Some(new_custodian) = lockup.custodian {
account_metas.push(AccountMeta::new_readonly(new_custodian, true));
}
Instruction::new_with_bincode(
id(),
&StakeInstruction::SetLockupChecked(lockup_checked),
account_metas,
)
}
pub fn process_instruction(
_program_id: &Pubkey,
keyed_accounts: &[KeyedAccount],
@ -598,7 +801,6 @@ pub fn process_instruction(
can_merge_expired_lockups,
)
}
StakeInstruction::Withdraw(lamports) => {
let to = &next_keyed_account(keyed_accounts)?;
me.withdraw(
@ -615,7 +817,6 @@ pub fn process_instruction(
&from_keyed_account::<Clock>(next_keyed_account(keyed_accounts)?)?,
&signers,
),
StakeInstruction::SetLockup(lockup) => {
let clock = if invoke_context.is_feature_active(&feature_set::stake_program_v4::id()) {
Some(get_sysvar::<Clock>(invoke_context, &sysvar::clock::id())?)
@ -624,12 +825,107 @@ pub fn process_instruction(
};
me.set_lockup(&lockup, &signers, clock.as_ref())
}
StakeInstruction::InitializeChecked => {
if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id())
{
let rent = next_keyed_account(keyed_accounts)?;
let staker = next_keyed_account(keyed_accounts)?;
let withdrawer = next_keyed_account(keyed_accounts)?;
let authorized = Authorized {
staker: *staker.unsigned_key(),
withdrawer: *withdrawer
.signer_key()
.ok_or(InstructionError::MissingRequiredSignature)?,
};
me.initialize(
&authorized,
&Lockup::default(),
&from_keyed_account::<Rent>(rent)?,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
StakeInstruction::AuthorizeChecked(stake_authorize) => {
if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id())
{
let clock = from_keyed_account::<Clock>(next_keyed_account(keyed_accounts)?)?;
let _current_authority = next_keyed_account(keyed_accounts)?;
let authorized_pubkey = &next_keyed_account(keyed_accounts)?
.signer_key()
.ok_or(InstructionError::MissingRequiredSignature)?;
let custodian = keyed_accounts.next().map(|ka| ka.unsigned_key());
me.authorize(
&signers,
authorized_pubkey,
stake_authorize,
true,
&clock,
custodian,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
StakeInstruction::AuthorizeCheckedWithSeed(args) => {
if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id())
{
let authority_base = next_keyed_account(keyed_accounts)?;
let clock = from_keyed_account::<Clock>(next_keyed_account(keyed_accounts)?)?;
let authorized_pubkey = &next_keyed_account(keyed_accounts)?
.signer_key()
.ok_or(InstructionError::MissingRequiredSignature)?;
let custodian = keyed_accounts.next().map(|ka| ka.unsigned_key());
me.authorize_with_seed(
authority_base,
&args.authority_seed,
&args.authority_owner,
authorized_pubkey,
args.stake_authorize,
true,
&clock,
custodian,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
StakeInstruction::SetLockupChecked(lockup_checked) => {
if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id())
{
let _current_authority = next_keyed_account(keyed_accounts)?;
let custodian = if let Some(custodian) = keyed_accounts.next() {
Some(
*custodian
.signer_key()
.ok_or(InstructionError::MissingRequiredSignature)?,
)
} else {
None
};
let lockup = LockupArgs {
unix_timestamp: lockup_checked.unix_timestamp,
epoch: lockup_checked.epoch,
custodian,
};
let clock = Some(get_sysvar::<Clock>(invoke_context, &sysvar::clock::id())?);
me.set_lockup(&lockup, &signers, clock.as_ref())
} else {
Err(InstructionError::InvalidInstructionData)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stake_state::{Meta, StakeState};
use bincode::serialize;
use solana_sdk::{
account::{self, Account, AccountSharedData},
@ -637,8 +933,7 @@ mod tests {
rent::Rent,
sysvar::stake_history::StakeHistory,
};
use std::cell::RefCell;
use std::str::FromStr;
use std::{cell::RefCell, rc::Rc, str::FromStr};
fn create_default_account() -> RefCell<AccountSharedData> {
RefCell::new(AccountSharedData::default())
@ -1173,4 +1468,276 @@ mod tests {
pretty_err::<StakeError>(StakeError::NoCreditsToRedeem.into())
)
}
#[test]
fn test_stake_checked_instructions() {
let stake_address = Pubkey::new_unique();
let staker = Pubkey::new_unique();
let withdrawer = Pubkey::new_unique();
// Test InitializeChecked with non-signing withdrawer
let mut instruction =
initialize_checked(&stake_address, &Authorized { staker, withdrawer });
instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
// Test InitializeChecked with withdrawer signer
let stake_account = AccountSharedData::new_ref(
1_000_000_000,
std::mem::size_of::<crate::stake_state::StakeState>(),
&id(),
);
let rent_address = sysvar::rent::id();
let rent_account = RefCell::new(account::create_account_shared_data_for_test(
&Rent::default(),
));
let staker_account = create_default_account();
let withdrawer_account = create_default_account();
let keyed_accounts = vec![
KeyedAccount::new(&stake_address, false, &stake_account),
KeyedAccount::new(&rent_address, false, &rent_account),
KeyedAccount::new(&staker, false, &staker_account),
KeyedAccount::new(&withdrawer, true, &withdrawer_account),
];
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&StakeInstruction::InitializeChecked).unwrap(),
&mut MockInvokeContext::default()
),
Ok(()),
);
// Test AuthorizeChecked with non-signing authority
let authorized_address = Pubkey::new_unique();
let mut instruction = authorize_checked(
&stake_address,
&authorized_address,
&staker,
StakeAuthorize::Staker,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(staker, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
let mut instruction = authorize_checked(
&stake_address,
&authorized_address,
&withdrawer,
StakeAuthorize::Withdrawer,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
// Test AuthorizeChecked with authority signer
let stake_account = AccountSharedData::new_ref_data_with_space(
42,
&StakeState::Initialized(Meta::auto(&authorized_address)),
std::mem::size_of::<StakeState>(),
&id(),
)
.unwrap();
let clock_address = sysvar::clock::id();
let clock_account = RefCell::new(account::create_account_shared_data_for_test(
&Clock::default(),
));
let authorized_account = create_default_account();
let new_authorized_account = create_default_account();
let keyed_accounts = vec![
KeyedAccount::new(&stake_address, false, &stake_account),
KeyedAccount::new(&clock_address, false, &clock_account),
KeyedAccount::new(&authorized_address, true, &authorized_account),
KeyedAccount::new(&staker, true, &new_authorized_account),
];
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&StakeInstruction::AuthorizeChecked(StakeAuthorize::Staker)).unwrap(),
&mut MockInvokeContext::default()
),
Ok(()),
);
let keyed_accounts = vec![
KeyedAccount::new(&stake_address, false, &stake_account),
KeyedAccount::new(&clock_address, false, &clock_account),
KeyedAccount::new(&authorized_address, true, &authorized_account),
KeyedAccount::new(&withdrawer, true, &new_authorized_account),
];
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&StakeInstruction::AuthorizeChecked(
StakeAuthorize::Withdrawer
))
.unwrap(),
&mut MockInvokeContext::default()
),
Ok(()),
);
// Test AuthorizeCheckedWithSeed with non-signing authority
let authorized_owner = Pubkey::new_unique();
let seed = "test seed";
let address_with_seed =
Pubkey::create_with_seed(&authorized_owner, seed, &authorized_owner).unwrap();
let mut instruction = authorize_checked_with_seed(
&stake_address,
&authorized_owner,
seed.to_string(),
&authorized_owner,
&staker,
StakeAuthorize::Staker,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(staker, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
let mut instruction = authorize_checked_with_seed(
&stake_address,
&authorized_owner,
seed.to_string(),
&authorized_owner,
&staker,
StakeAuthorize::Withdrawer,
None,
);
instruction.accounts[3] = AccountMeta::new_readonly(staker, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
// Test AuthorizeCheckedWithSeed with authority signer
let stake_account = AccountSharedData::new_ref_data_with_space(
42,
&StakeState::Initialized(Meta::auto(&address_with_seed)),
std::mem::size_of::<StakeState>(),
&id(),
)
.unwrap();
let keyed_accounts = vec![
KeyedAccount::new(&address_with_seed, false, &stake_account),
KeyedAccount::new(&authorized_owner, true, &authorized_account),
KeyedAccount::new(&clock_address, false, &clock_account),
KeyedAccount::new(&staker, true, &new_authorized_account),
];
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&StakeInstruction::AuthorizeCheckedWithSeed(
AuthorizeCheckedWithSeedArgs {
stake_authorize: StakeAuthorize::Staker,
authority_seed: seed.to_string(),
authority_owner: authorized_owner,
}
))
.unwrap(),
&mut MockInvokeContext::default()
),
Ok(()),
);
let keyed_accounts = vec![
KeyedAccount::new(&address_with_seed, false, &stake_account),
KeyedAccount::new(&authorized_owner, true, &authorized_account),
KeyedAccount::new(&clock_address, false, &clock_account),
KeyedAccount::new(&withdrawer, true, &new_authorized_account),
];
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&StakeInstruction::AuthorizeCheckedWithSeed(
AuthorizeCheckedWithSeedArgs {
stake_authorize: StakeAuthorize::Withdrawer,
authority_seed: seed.to_string(),
authority_owner: authorized_owner,
}
))
.unwrap(),
&mut MockInvokeContext::default()
),
Ok(()),
);
// Test SetLockupChecked with non-signing lockup custodian
let custodian = Pubkey::new_unique();
let mut instruction = set_lockup_checked(
&stake_address,
&LockupArgs {
unix_timestamp: None,
epoch: Some(1),
custodian: Some(custodian),
},
&withdrawer,
);
instruction.accounts[2] = AccountMeta::new_readonly(custodian, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
// Test SetLockupChecked with lockup custodian signer
let stake_account = AccountSharedData::new_ref_data_with_space(
42,
&StakeState::Initialized(Meta::auto(&withdrawer)),
std::mem::size_of::<StakeState>(),
&id(),
)
.unwrap();
let custodian_account = create_default_account();
let keyed_accounts = vec![
KeyedAccount::new(&stake_address, false, &stake_account),
KeyedAccount::new(&withdrawer, true, &withdrawer_account),
KeyedAccount::new(&custodian, true, &custodian_account),
];
let mut invoke_context = MockInvokeContext::default();
let clock = Clock::default();
let mut data = vec![];
bincode::serialize_into(&mut data, &clock).unwrap();
invoke_context
.sysvars
.push((sysvar::clock::id(), Some(Rc::new(data))));
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&StakeInstruction::SetLockupChecked(LockupCheckedArgs {
unix_timestamp: None,
epoch: Some(1),
}))
.unwrap(),
&mut invoke_context
),
Ok(()),
);
}
}

View File

@ -111,6 +111,18 @@ pub enum VoteInstruction {
/// 2. [] Clock sysvar
/// 3. [SIGNER] Vote authority
VoteSwitch(Vote, Hash),
/// Authorize a key to send votes or issue a withdrawal
///
/// This instruction behaves like `Authorize` with the additional requirement that the new vote
/// or withdraw authority must also be a signer.
///
/// # Account references
/// 0. [WRITE] Vote account to be updated with the Pubkey for authorization
/// 1. [] Clock sysvar
/// 2. [SIGNER] Vote or withdraw authority
/// 3. [SIGNER] New vote or withdraw authority
AuthorizeChecked(VoteAuthorize),
}
fn initialize_account(vote_pubkey: &Pubkey, vote_init: &VoteInit) -> Instruction {
@ -182,6 +194,26 @@ pub fn authorize(
)
}
pub fn authorize_checked(
vote_pubkey: &Pubkey,
authorized_pubkey: &Pubkey, // currently authorized
new_authorized_pubkey: &Pubkey,
vote_authorize: VoteAuthorize,
) -> Instruction {
let account_metas = vec![
AccountMeta::new(*vote_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(*authorized_pubkey, true),
AccountMeta::new_readonly(*new_authorized_pubkey, true),
];
Instruction::new_with_bincode(
id(),
&VoteInstruction::AuthorizeChecked(vote_authorize),
account_metas,
)
}
pub fn update_validator_identity(
vote_pubkey: &Pubkey,
authorized_withdrawer_pubkey: &Pubkey,
@ -335,12 +367,32 @@ pub fn process_instruction(
let to = next_keyed_account(keyed_accounts)?;
vote_state::withdraw(me, lamports, to, &signers)
}
VoteInstruction::AuthorizeChecked(vote_authorize) => {
if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id())
{
let clock = next_keyed_account(keyed_accounts)?;
let _current_authority = next_keyed_account(keyed_accounts)?;
let voter_pubkey = &next_keyed_account(keyed_accounts)?
.signer_key()
.ok_or(InstructionError::MissingRequiredSignature)?;
vote_state::authorize(
me,
voter_pubkey,
vote_authorize,
&signers,
&from_keyed_account::<Clock>(clock)?,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bincode::serialize;
use solana_sdk::{
account::{self, Account, AccountSharedData},
process_instruction::MockInvokeContext,
@ -349,6 +401,10 @@ mod tests {
use std::cell::RefCell;
use std::str::FromStr;
fn create_default_account() -> RefCell<AccountSharedData> {
RefCell::new(AccountSharedData::default())
}
// these are for 100% coverage in this file
#[test]
fn test_vote_process_instruction_decode_bail() {
@ -491,6 +547,107 @@ mod tests {
);
}
#[test]
fn test_vote_authorize_checked() {
let vote_pubkey = Pubkey::new_unique();
let authorized_pubkey = Pubkey::new_unique();
let new_authorized_pubkey = Pubkey::new_unique();
// Test with vanilla authorize accounts
let mut instruction = authorize_checked(
&vote_pubkey,
&authorized_pubkey,
&new_authorized_pubkey,
VoteAuthorize::Voter,
);
instruction.accounts = instruction.accounts[0..2].to_vec();
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::NotEnoughAccountKeys),
);
let mut instruction = authorize_checked(
&vote_pubkey,
&authorized_pubkey,
&new_authorized_pubkey,
VoteAuthorize::Withdrawer,
);
instruction.accounts = instruction.accounts[0..2].to_vec();
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::NotEnoughAccountKeys),
);
// Test with non-signing new_authorized_pubkey
let mut instruction = authorize_checked(
&vote_pubkey,
&authorized_pubkey,
&new_authorized_pubkey,
VoteAuthorize::Voter,
);
instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
let mut instruction = authorize_checked(
&vote_pubkey,
&authorized_pubkey,
&new_authorized_pubkey,
VoteAuthorize::Withdrawer,
);
instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false);
assert_eq!(
process_instruction(&instruction),
Err(InstructionError::MissingRequiredSignature),
);
// Test with new_authorized_pubkey signer
let vote_account = AccountSharedData::new_ref(100, VoteState::size_of(), &id());
let clock_address = sysvar::clock::id();
let clock_account = RefCell::new(account::create_account_shared_data_for_test(
&Clock::default(),
));
let default_authorized_pubkey = Pubkey::default();
let authorized_account = create_default_account();
let new_authorized_account = create_default_account();
let keyed_accounts = vec![
KeyedAccount::new(&vote_pubkey, false, &vote_account),
KeyedAccount::new(&clock_address, false, &clock_account),
KeyedAccount::new(&default_authorized_pubkey, true, &authorized_account),
KeyedAccount::new(&new_authorized_pubkey, true, &new_authorized_account),
];
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&VoteInstruction::AuthorizeChecked(VoteAuthorize::Voter)).unwrap(),
&mut MockInvokeContext::default()
),
Ok(())
);
let keyed_accounts = vec![
KeyedAccount::new(&vote_pubkey, false, &vote_account),
KeyedAccount::new(&clock_address, false, &clock_account),
KeyedAccount::new(&default_authorized_pubkey, true, &authorized_account),
KeyedAccount::new(&new_authorized_pubkey, true, &new_authorized_account),
];
assert_eq!(
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&serialize(&VoteInstruction::AuthorizeChecked(
VoteAuthorize::Withdrawer
))
.unwrap(),
&mut MockInvokeContext::default()
),
Ok(())
);
}
#[test]
fn test_minimum_balance() {
let rent = solana_sdk::rent::Rent::default();

View File

@ -154,6 +154,10 @@ pub mod dedupe_config_program_signers {
solana_sdk::declare_id!("8kEuAshXLsgkUEdcFVLqrjCGGHVWFW99ZZpxvAzzMtBp");
}
pub mod vote_stake_checked_instructions {
solana_sdk::declare_id!("BcWknVcgvonN8sL4HE4XFuEVgfcee5MwxWPAgP6ZV89X");
}
lazy_static! {
/// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
@ -191,6 +195,7 @@ lazy_static! {
(system_transfer_zero_check::id(), "perform all checks for transfers of 0 lamports"),
(memory_ops_syscalls::id(), "add syscalls for memory operations"),
(dedupe_config_program_signers::id(), "dedupe config program signers"),
(vote_stake_checked_instructions::id(), "vote/state program checked instructions #18345"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()

View File

@ -185,6 +185,86 @@ pub fn parse_stake(
info: value,
})
}
StakeInstruction::InitializeChecked => {
check_num_stake_accounts(&instruction.accounts, 4)?;
Ok(ParsedInstructionEnum {
instruction_type: "initializeChecked".to_string(),
info: json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"rentSysvar": account_keys[instruction.accounts[1] as usize].to_string(),
"staker": account_keys[instruction.accounts[2] as usize].to_string(),
"withdrawer": account_keys[instruction.accounts[3] as usize].to_string(),
}),
})
}
StakeInstruction::AuthorizeChecked(authority_type) => {
check_num_stake_accounts(&instruction.accounts, 4)?;
let mut value = json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(),
"authority": account_keys[instruction.accounts[2] as usize].to_string(),
"newAuthority": account_keys[instruction.accounts[3] as usize].to_string(),
"authorityType": authority_type,
});
let map = value.as_object_mut().unwrap();
if instruction.accounts.len() >= 5 {
map.insert(
"custodian".to_string(),
json!(account_keys[instruction.accounts[4] as usize].to_string()),
);
}
Ok(ParsedInstructionEnum {
instruction_type: "authorizeChecked".to_string(),
info: value,
})
}
StakeInstruction::AuthorizeCheckedWithSeed(args) => {
check_num_stake_accounts(&instruction.accounts, 4)?;
let mut value = json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"authorityBase": account_keys[instruction.accounts[1] as usize].to_string(),
"clockSysvar": account_keys[instruction.accounts[2] as usize].to_string(),
"newAuthorized": account_keys[instruction.accounts[3] as usize].to_string(),
"authorityType": args.stake_authorize,
"authoritySeed": args.authority_seed,
"authorityOwner": args.authority_owner.to_string(),
});
let map = value.as_object_mut().unwrap();
if instruction.accounts.len() >= 5 {
map.insert(
"custodian".to_string(),
json!(account_keys[instruction.accounts[4] as usize].to_string()),
);
}
Ok(ParsedInstructionEnum {
instruction_type: "authorizeCheckedWithSeed".to_string(),
info: value,
})
}
StakeInstruction::SetLockupChecked(lockup_args) => {
check_num_stake_accounts(&instruction.accounts, 2)?;
let mut lockup_map = Map::new();
if let Some(timestamp) = lockup_args.unix_timestamp {
lockup_map.insert("unixTimestamp".to_string(), json!(timestamp));
}
if let Some(epoch) = lockup_args.epoch {
lockup_map.insert("epoch".to_string(), json!(epoch));
}
if instruction.accounts.len() >= 3 {
lockup_map.insert(
"custodian".to_string(),
json!(account_keys[instruction.accounts[2] as usize].to_string()),
);
}
Ok(ParsedInstructionEnum {
instruction_type: "setLockupChecked".to_string(),
info: json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"custodian": account_keys[instruction.accounts[1] as usize].to_string(),
"lockup": lockup_map,
}),
})
}
}
}
@ -268,22 +348,22 @@ mod test {
assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err());
let instruction = stake_instruction::authorize(
&keys[1],
&keys[2],
&keys[0],
&keys[3],
&keys[4],
StakeAuthorize::Withdrawer,
Some(&keys[1]),
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..3]).unwrap(),
parse_stake(&message.instructions[0], &keys[0..4]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorize".to_string(),
info: json!({
"stakeAccount": keys[1].to_string(),
"clockSysvar": keys[2].to_string(),
"stakeAccount": keys[2].to_string(),
"clockSysvar": keys[3].to_string(),
"authority": keys[0].to_string(),
"newAuthority": keys[3].to_string(),
"newAuthority": keys[4].to_string(),
"authorityType": StakeAuthorize::Withdrawer,
"custodian": keys[1].to_string(),
}),
@ -402,7 +482,7 @@ mod test {
&keys[1],
&keys[0],
seed.to_string(),
&keys[2],
&keys[0],
&keys[3],
StakeAuthorize::Staker,
None,
@ -414,7 +494,7 @@ mod test {
instruction_type: "authorizeWithSeed".to_string(),
info: json!({
"stakeAccount": keys[1].to_string(),
"authorityOwner": keys[2].to_string(),
"authorityOwner": keys[0].to_string(),
"newAuthorized": keys[3].to_string(),
"authorityBase": keys[0].to_string(),
"authoritySeed": seed,
@ -423,26 +503,26 @@ mod test {
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err());
assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err());
let instruction = stake_instruction::authorize_with_seed(
&keys[1],
&keys[2],
&keys[0],
seed.to_string(),
&keys[2],
&keys[3],
&keys[0],
&keys[4],
StakeAuthorize::Withdrawer,
Some(&keys[4]),
Some(&keys[1]),
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..5]).unwrap(),
parse_stake(&message.instructions[0], &keys[0..4]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorizeWithSeed".to_string(),
info: json!({
"stakeAccount": keys[2].to_string(),
"authorityOwner": keys[2].to_string(),
"newAuthorized": keys[3].to_string(),
"authorityOwner": keys[0].to_string(),
"newAuthorized": keys[4].to_string(),
"authorityBase": keys[0].to_string(),
"authoritySeed": seed,
"authorityType": StakeAuthorize::Withdrawer,
@ -451,14 +531,14 @@ mod test {
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err());
assert!(parse_stake(&message.instructions[0], &keys[0..3]).is_err());
}
#[test]
#[allow(clippy::same_item_push)]
fn test_parse_set_lockup() {
fn test_parse_stake_set_lockup() {
let mut keys: Vec<Pubkey> = vec![];
for _ in 0..2 {
for _ in 0..3 {
keys.push(Pubkey::new_unique());
}
let unix_timestamp = 1_234_567_890;
@ -532,5 +612,208 @@ mod test {
);
assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err());
let lockup = LockupArgs {
unix_timestamp: Some(unix_timestamp),
epoch: None,
custodian: None,
};
let instruction = stake_instruction::set_lockup_checked(&keys[1], &lockup, &keys[0]);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..2]).unwrap(),
ParsedInstructionEnum {
instruction_type: "setLockupChecked".to_string(),
info: json!({
"stakeAccount": keys[1].to_string(),
"custodian": keys[0].to_string(),
"lockup": {
"unixTimestamp": unix_timestamp
}
}),
}
);
let lockup = LockupArgs {
unix_timestamp: Some(unix_timestamp),
epoch: Some(epoch),
custodian: None,
};
let instruction = stake_instruction::set_lockup_checked(&keys[1], &lockup, &keys[0]);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..2]).unwrap(),
ParsedInstructionEnum {
instruction_type: "setLockupChecked".to_string(),
info: json!({
"stakeAccount": keys[1].to_string(),
"custodian": keys[0].to_string(),
"lockup": {
"unixTimestamp": unix_timestamp,
"epoch": epoch,
}
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err());
let lockup = LockupArgs {
unix_timestamp: Some(unix_timestamp),
epoch: Some(epoch),
custodian: Some(keys[1]),
};
let instruction = stake_instruction::set_lockup_checked(&keys[2], &lockup, &keys[0]);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..3]).unwrap(),
ParsedInstructionEnum {
instruction_type: "setLockupChecked".to_string(),
info: json!({
"stakeAccount": keys[2].to_string(),
"custodian": keys[0].to_string(),
"lockup": {
"unixTimestamp": unix_timestamp,
"epoch": epoch,
"custodian": keys[1].to_string(),
}
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err());
}
#[test]
#[allow(clippy::same_item_push)]
fn test_parse_stake_checked_instructions() {
let mut keys: Vec<Pubkey> = vec![];
for _ in 0..6 {
keys.push(Pubkey::new_unique());
}
let authorized = Authorized {
staker: keys[3],
withdrawer: keys[0],
};
let lamports = 55;
let instructions =
stake_instruction::create_account_checked(&keys[0], &keys[1], &authorized, lamports);
let message = Message::new(&instructions, None);
assert_eq!(
parse_stake(&message.instructions[1], &keys[0..4]).unwrap(),
ParsedInstructionEnum {
instruction_type: "initializeChecked".to_string(),
info: json!({
"stakeAccount": keys[1].to_string(),
"rentSysvar": keys[2].to_string(),
"staker": keys[3].to_string(),
"withdrawer": keys[0].to_string(),
}),
}
);
assert!(parse_stake(&message.instructions[1], &keys[0..3]).is_err());
let instruction = stake_instruction::authorize_checked(
&keys[2],
&keys[0],
&keys[1],
StakeAuthorize::Staker,
None,
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..4]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorizeChecked".to_string(),
info: json!({
"stakeAccount": keys[2].to_string(),
"clockSysvar": keys[3].to_string(),
"authority": keys[0].to_string(),
"newAuthority": keys[1].to_string(),
"authorityType": StakeAuthorize::Staker,
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..3]).is_err());
let instruction = stake_instruction::authorize_checked(
&keys[3],
&keys[0],
&keys[1],
StakeAuthorize::Withdrawer,
Some(&keys[2]),
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..5]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorizeChecked".to_string(),
info: json!({
"stakeAccount": keys[3].to_string(),
"clockSysvar": keys[4].to_string(),
"authority": keys[0].to_string(),
"newAuthority": keys[1].to_string(),
"authorityType": StakeAuthorize::Withdrawer,
"custodian": keys[2].to_string(),
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..4]).is_err());
let seed = "test_seed";
let instruction = stake_instruction::authorize_checked_with_seed(
&keys[2],
&keys[0],
seed.to_string(),
&keys[0],
&keys[1],
StakeAuthorize::Staker,
None,
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..4]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorizeCheckedWithSeed".to_string(),
info: json!({
"stakeAccount": keys[2].to_string(),
"authorityOwner": keys[0].to_string(),
"newAuthorized": keys[1].to_string(),
"authorityBase": keys[0].to_string(),
"authoritySeed": seed,
"authorityType": StakeAuthorize::Staker,
"clockSysvar": keys[3].to_string(),
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..3]).is_err());
let instruction = stake_instruction::authorize_checked_with_seed(
&keys[3],
&keys[0],
seed.to_string(),
&keys[0],
&keys[1],
StakeAuthorize::Withdrawer,
Some(&keys[2]),
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..5]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorizeCheckedWithSeed".to_string(),
info: json!({
"stakeAccount": keys[3].to_string(),
"authorityOwner": keys[0].to_string(),
"newAuthorized": keys[1].to_string(),
"authorityBase": keys[0].to_string(),
"authoritySeed": seed,
"authorityType": StakeAuthorize::Withdrawer,
"clockSysvar": keys[4].to_string(),
"custodian": keys[2].to_string(),
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..4]).is_err());
}
}

View File

@ -121,6 +121,19 @@ pub fn parse_vote(
}),
})
}
VoteInstruction::AuthorizeChecked(authority_type) => {
check_num_vote_accounts(&instruction.accounts, 4)?;
Ok(ParsedInstructionEnum {
instruction_type: "authorizeChecked".to_string(),
info: json!({
"voteAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(),
"authority": account_keys[instruction.accounts[2] as usize].to_string(),
"newAuthority": account_keys[instruction.accounts[3] as usize].to_string(),
"authorityType": authority_type,
}),
})
}
}
}
@ -294,5 +307,24 @@ mod test {
}
);
assert!(parse_vote(&message.instructions[0], &keys[0..3]).is_err());
let authority_type = VoteAuthorize::Voter;
let instruction =
vote_instruction::authorize_checked(&keys[1], &keys[0], &keys[3], authority_type);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_vote(&message.instructions[0], &keys[0..4]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorizeChecked".to_string(),
info: json!({
"voteAccount": keys[2].to_string(),
"clockSysvar": keys[3].to_string(),
"authority": keys[0].to_string(),
"newAuthority": keys[1].to_string(),
"authorityType": authority_type,
}),
}
);
assert!(parse_vote(&message.instructions[0], &keys[0..3]).is_err());
}
}