* Add vote/stake checked instructions (cherry picked from commitee219ffa47
) # 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 commit544f62c92f
) * Add parsing for new stake instructions; clean up confusing test args (cherry picked from commit9b302ac0b5
) # Conflicts: # transaction-status/src/parse_stake.rs * Add parsing for new vote instructions (cherry picked from commit39bac256ab
) * Add VoteInstruction::AuthorizeChecked test (cherry picked from commitb8ca2250fd
) * Add Stake checked tests (cherry picked from commit74e89a3e3e
) # 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:
@ -169,6 +169,61 @@ pub enum StakeInstruction {
|
|||||||
/// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
|
/// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
|
||||||
/// lockup expiration
|
/// lockup expiration
|
||||||
AuthorizeWithSeed(AuthorizeWithSeedArgs),
|
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)]
|
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||||
@ -178,6 +233,12 @@ pub struct LockupArgs {
|
|||||||
pub custodian: Option<Pubkey>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||||
pub struct AuthorizeWithSeedArgs {
|
pub struct AuthorizeWithSeedArgs {
|
||||||
pub new_authorized_pubkey: Pubkey,
|
pub new_authorized_pubkey: Pubkey,
|
||||||
@ -186,6 +247,13 @@ pub struct AuthorizeWithSeedArgs {
|
|||||||
pub authority_owner: Pubkey,
|
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 {
|
pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction {
|
||||||
Instruction::new_with_bincode(
|
Instruction::new_with_bincode(
|
||||||
id(),
|
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(
|
pub fn create_account_with_seed(
|
||||||
from_pubkey: &Pubkey,
|
from_pubkey: &Pubkey,
|
||||||
stake_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(
|
pub fn create_account(
|
||||||
from_pubkey: &Pubkey,
|
from_pubkey: &Pubkey,
|
||||||
stake_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(
|
pub fn authorize_with_seed(
|
||||||
stake_pubkey: &Pubkey,
|
stake_pubkey: &Pubkey,
|
||||||
authority_base: &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(
|
pub fn delegate_stake(
|
||||||
stake_pubkey: &Pubkey,
|
stake_pubkey: &Pubkey,
|
||||||
authorized_pubkey: &Pubkey,
|
authorized_pubkey: &Pubkey,
|
||||||
@ -477,6 +656,30 @@ pub fn set_lockup(
|
|||||||
Instruction::new_with_bincode(id(), &StakeInstruction::SetLockup(*lockup), account_metas)
|
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(
|
pub fn process_instruction(
|
||||||
_program_id: &Pubkey,
|
_program_id: &Pubkey,
|
||||||
keyed_accounts: &[KeyedAccount],
|
keyed_accounts: &[KeyedAccount],
|
||||||
@ -598,7 +801,6 @@ pub fn process_instruction(
|
|||||||
can_merge_expired_lockups,
|
can_merge_expired_lockups,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
StakeInstruction::Withdraw(lamports) => {
|
StakeInstruction::Withdraw(lamports) => {
|
||||||
let to = &next_keyed_account(keyed_accounts)?;
|
let to = &next_keyed_account(keyed_accounts)?;
|
||||||
me.withdraw(
|
me.withdraw(
|
||||||
@ -615,7 +817,6 @@ pub fn process_instruction(
|
|||||||
&from_keyed_account::<Clock>(next_keyed_account(keyed_accounts)?)?,
|
&from_keyed_account::<Clock>(next_keyed_account(keyed_accounts)?)?,
|
||||||
&signers,
|
&signers,
|
||||||
),
|
),
|
||||||
|
|
||||||
StakeInstruction::SetLockup(lockup) => {
|
StakeInstruction::SetLockup(lockup) => {
|
||||||
let clock = if invoke_context.is_feature_active(&feature_set::stake_program_v4::id()) {
|
let clock = if invoke_context.is_feature_active(&feature_set::stake_program_v4::id()) {
|
||||||
Some(get_sysvar::<Clock>(invoke_context, &sysvar::clock::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())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::stake_state::{Meta, StakeState};
|
||||||
use bincode::serialize;
|
use bincode::serialize;
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
account::{self, Account, AccountSharedData},
|
account::{self, Account, AccountSharedData},
|
||||||
@ -637,8 +933,7 @@ mod tests {
|
|||||||
rent::Rent,
|
rent::Rent,
|
||||||
sysvar::stake_history::StakeHistory,
|
sysvar::stake_history::StakeHistory,
|
||||||
};
|
};
|
||||||
use std::cell::RefCell;
|
use std::{cell::RefCell, rc::Rc, str::FromStr};
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
fn create_default_account() -> RefCell<AccountSharedData> {
|
fn create_default_account() -> RefCell<AccountSharedData> {
|
||||||
RefCell::new(AccountSharedData::default())
|
RefCell::new(AccountSharedData::default())
|
||||||
@ -1173,4 +1468,276 @@ mod tests {
|
|||||||
pretty_err::<StakeError>(StakeError::NoCreditsToRedeem.into())
|
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(()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,18 @@ pub enum VoteInstruction {
|
|||||||
/// 2. [] Clock sysvar
|
/// 2. [] Clock sysvar
|
||||||
/// 3. [SIGNER] Vote authority
|
/// 3. [SIGNER] Vote authority
|
||||||
VoteSwitch(Vote, Hash),
|
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 {
|
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(
|
pub fn update_validator_identity(
|
||||||
vote_pubkey: &Pubkey,
|
vote_pubkey: &Pubkey,
|
||||||
authorized_withdrawer_pubkey: &Pubkey,
|
authorized_withdrawer_pubkey: &Pubkey,
|
||||||
@ -335,12 +367,32 @@ pub fn process_instruction(
|
|||||||
let to = next_keyed_account(keyed_accounts)?;
|
let to = next_keyed_account(keyed_accounts)?;
|
||||||
vote_state::withdraw(me, lamports, to, &signers)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use bincode::serialize;
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
account::{self, Account, AccountSharedData},
|
account::{self, Account, AccountSharedData},
|
||||||
process_instruction::MockInvokeContext,
|
process_instruction::MockInvokeContext,
|
||||||
@ -349,6 +401,10 @@ mod tests {
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
fn create_default_account() -> RefCell<AccountSharedData> {
|
||||||
|
RefCell::new(AccountSharedData::default())
|
||||||
|
}
|
||||||
|
|
||||||
// these are for 100% coverage in this file
|
// these are for 100% coverage in this file
|
||||||
#[test]
|
#[test]
|
||||||
fn test_vote_process_instruction_decode_bail() {
|
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]
|
#[test]
|
||||||
fn test_minimum_balance() {
|
fn test_minimum_balance() {
|
||||||
let rent = solana_sdk::rent::Rent::default();
|
let rent = solana_sdk::rent::Rent::default();
|
||||||
|
@ -154,6 +154,10 @@ pub mod dedupe_config_program_signers {
|
|||||||
solana_sdk::declare_id!("8kEuAshXLsgkUEdcFVLqrjCGGHVWFW99ZZpxvAzzMtBp");
|
solana_sdk::declare_id!("8kEuAshXLsgkUEdcFVLqrjCGGHVWFW99ZZpxvAzzMtBp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod vote_stake_checked_instructions {
|
||||||
|
solana_sdk::declare_id!("BcWknVcgvonN8sL4HE4XFuEVgfcee5MwxWPAgP6ZV89X");
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Map of feature identifiers to user-visible description
|
/// Map of feature identifiers to user-visible description
|
||||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
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"),
|
(system_transfer_zero_check::id(), "perform all checks for transfers of 0 lamports"),
|
||||||
(memory_ops_syscalls::id(), "add syscalls for memory operations"),
|
(memory_ops_syscalls::id(), "add syscalls for memory operations"),
|
||||||
(dedupe_config_program_signers::id(), "dedupe config program signers"),
|
(dedupe_config_program_signers::id(), "dedupe config program signers"),
|
||||||
|
(vote_stake_checked_instructions::id(), "vote/state program checked instructions #18345"),
|
||||||
/*************** ADD NEW FEATURES HERE ***************/
|
/*************** ADD NEW FEATURES HERE ***************/
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -185,6 +185,86 @@ pub fn parse_stake(
|
|||||||
info: value,
|
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());
|
assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err());
|
||||||
|
|
||||||
let instruction = stake_instruction::authorize(
|
let instruction = stake_instruction::authorize(
|
||||||
&keys[1],
|
&keys[2],
|
||||||
&keys[0],
|
&keys[0],
|
||||||
&keys[3],
|
&keys[4],
|
||||||
StakeAuthorize::Withdrawer,
|
StakeAuthorize::Withdrawer,
|
||||||
Some(&keys[1]),
|
Some(&keys[1]),
|
||||||
);
|
);
|
||||||
let message = Message::new(&[instruction], None);
|
let message = Message::new(&[instruction], None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_stake(&message.instructions[0], &keys[0..3]).unwrap(),
|
parse_stake(&message.instructions[0], &keys[0..4]).unwrap(),
|
||||||
ParsedInstructionEnum {
|
ParsedInstructionEnum {
|
||||||
instruction_type: "authorize".to_string(),
|
instruction_type: "authorize".to_string(),
|
||||||
info: json!({
|
info: json!({
|
||||||
"stakeAccount": keys[1].to_string(),
|
"stakeAccount": keys[2].to_string(),
|
||||||
"clockSysvar": keys[2].to_string(),
|
"clockSysvar": keys[3].to_string(),
|
||||||
"authority": keys[0].to_string(),
|
"authority": keys[0].to_string(),
|
||||||
"newAuthority": keys[3].to_string(),
|
"newAuthority": keys[4].to_string(),
|
||||||
"authorityType": StakeAuthorize::Withdrawer,
|
"authorityType": StakeAuthorize::Withdrawer,
|
||||||
"custodian": keys[1].to_string(),
|
"custodian": keys[1].to_string(),
|
||||||
}),
|
}),
|
||||||
@ -402,7 +482,7 @@ mod test {
|
|||||||
&keys[1],
|
&keys[1],
|
||||||
&keys[0],
|
&keys[0],
|
||||||
seed.to_string(),
|
seed.to_string(),
|
||||||
&keys[2],
|
&keys[0],
|
||||||
&keys[3],
|
&keys[3],
|
||||||
StakeAuthorize::Staker,
|
StakeAuthorize::Staker,
|
||||||
None,
|
None,
|
||||||
@ -414,7 +494,7 @@ mod test {
|
|||||||
instruction_type: "authorizeWithSeed".to_string(),
|
instruction_type: "authorizeWithSeed".to_string(),
|
||||||
info: json!({
|
info: json!({
|
||||||
"stakeAccount": keys[1].to_string(),
|
"stakeAccount": keys[1].to_string(),
|
||||||
"authorityOwner": keys[2].to_string(),
|
"authorityOwner": keys[0].to_string(),
|
||||||
"newAuthorized": keys[3].to_string(),
|
"newAuthorized": keys[3].to_string(),
|
||||||
"authorityBase": keys[0].to_string(),
|
"authorityBase": keys[0].to_string(),
|
||||||
"authoritySeed": seed,
|
"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(
|
let instruction = stake_instruction::authorize_with_seed(
|
||||||
&keys[1],
|
&keys[2],
|
||||||
&keys[0],
|
&keys[0],
|
||||||
seed.to_string(),
|
seed.to_string(),
|
||||||
&keys[2],
|
&keys[0],
|
||||||
&keys[3],
|
&keys[4],
|
||||||
StakeAuthorize::Withdrawer,
|
StakeAuthorize::Withdrawer,
|
||||||
Some(&keys[4]),
|
Some(&keys[1]),
|
||||||
);
|
);
|
||||||
let message = Message::new(&[instruction], None);
|
let message = Message::new(&[instruction], None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_stake(&message.instructions[0], &keys[0..5]).unwrap(),
|
parse_stake(&message.instructions[0], &keys[0..4]).unwrap(),
|
||||||
ParsedInstructionEnum {
|
ParsedInstructionEnum {
|
||||||
instruction_type: "authorizeWithSeed".to_string(),
|
instruction_type: "authorizeWithSeed".to_string(),
|
||||||
info: json!({
|
info: json!({
|
||||||
"stakeAccount": keys[2].to_string(),
|
"stakeAccount": keys[2].to_string(),
|
||||||
"authorityOwner": keys[2].to_string(),
|
"authorityOwner": keys[0].to_string(),
|
||||||
"newAuthorized": keys[3].to_string(),
|
"newAuthorized": keys[4].to_string(),
|
||||||
"authorityBase": keys[0].to_string(),
|
"authorityBase": keys[0].to_string(),
|
||||||
"authoritySeed": seed,
|
"authoritySeed": seed,
|
||||||
"authorityType": StakeAuthorize::Withdrawer,
|
"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]
|
#[test]
|
||||||
#[allow(clippy::same_item_push)]
|
#[allow(clippy::same_item_push)]
|
||||||
fn test_parse_set_lockup() {
|
fn test_parse_stake_set_lockup() {
|
||||||
let mut keys: Vec<Pubkey> = vec![];
|
let mut keys: Vec<Pubkey> = vec![];
|
||||||
for _ in 0..2 {
|
for _ in 0..3 {
|
||||||
keys.push(Pubkey::new_unique());
|
keys.push(Pubkey::new_unique());
|
||||||
}
|
}
|
||||||
let unix_timestamp = 1_234_567_890;
|
let unix_timestamp = 1_234_567_890;
|
||||||
@ -532,5 +612,208 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user