diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index 56de93b4390422..5f0fbdea7c6079 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -119,7 +119,24 @@ pub fn process_instruction( } else { None }; - vote_state::withdraw(me, lamports, to, &signers, rent_sysvar.as_deref()) + + let clock_if_feature_active = if invoke_context + .feature_set + .is_active(&feature_set::reject_vote_account_close_unless_zero_credit_epoch::id()) + { + Some(invoke_context.get_sysvar_cache().get_clock()?) + } else { + None + }; + + vote_state::withdraw( + me, + lamports, + to, + &signers, + rent_sysvar.as_deref(), + clock_if_feature_active.as_deref(), + ) } VoteInstruction::AuthorizeChecked(vote_authorize) => { if invoke_context diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index a67749fef0d91b..ceaebe326992e8 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -1151,6 +1151,7 @@ pub fn withdraw( to_account: &KeyedAccount, signers: &HashSet, rent_sysvar: Option<&Rent>, + clock: Option<&Clock>, ) -> Result<(), InstructionError> { let vote_state: VoteState = State::::state(vote_account)?.convert_to_current(); @@ -1163,8 +1164,23 @@ pub fn withdraw( .ok_or(InstructionError::InsufficientFunds)?; if remaining_balance == 0 { - // Deinitialize upon zero-balance - vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?; + let reject_active_vote_account_close = clock + .zip(vote_state.epoch_credits.last()) + .map(|(clock, (last_epoch_with_credits, _, _))| { + let current_epoch = clock.epoch; + // if current_epoch - last_epoch_with_credits < 2 then the validator has received credits + // either in the current epoch or the previous epoch. If it's >= 2 then it has been at least + // one full epoch since the validator has received credits. + current_epoch.saturating_sub(*last_epoch_with_credits) < 2 + }) + .unwrap_or(false); + + if reject_active_vote_account_close { + return Err(InstructionError::ActiveVoteAccountClose); + } else { + // Deinitialize upon zero-balance + vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?; + } } else if let Some(rent_sysvar) = rent_sysvar { let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.data_len()?); if remaining_balance < min_rent_exempt_balance { @@ -1438,6 +1454,39 @@ mod tests { ) } + fn create_test_account_with_epoch_credits( + credits_to_append: &[u64], + ) -> (Pubkey, RefCell) { + let (vote_pubkey, vote_account) = create_test_account(); + let vote_account_space = vote_account.borrow().data().len(); + + let mut vote_state = VoteState::from(&*vote_account.borrow_mut()).unwrap(); + vote_state.authorized_withdrawer = vote_pubkey; + + vote_state.epoch_credits = Vec::new(); + + let mut current_epoch_credits = 0; + let mut previous_epoch_credits = 0; + for (epoch, credits) in credits_to_append.iter().enumerate() { + current_epoch_credits += credits; + vote_state.epoch_credits.push(( + u64::try_from(epoch).unwrap(), + current_epoch_credits, + previous_epoch_credits, + )); + previous_epoch_credits = current_epoch_credits; + } + + let lamports = vote_account.borrow().lamports(); + let mut vote_account_with_epoch_credits = + AccountSharedData::new(lamports, vote_account_space, &vote_pubkey); + let versioned = VoteStateVersions::new_current(vote_state); + VoteState::to(&versioned, &mut vote_account_with_epoch_credits); + let ref_vote_account_with_epoch_credits = RefCell::new(vote_account_with_epoch_credits); + + (vote_pubkey, ref_vote_account_with_epoch_credits) + } + fn simulate_process_vote( vote_pubkey: &Pubkey, vote_account: &RefCell, @@ -2222,6 +2271,13 @@ mod tests { #[test] fn test_vote_state_withdraw() { let (vote_pubkey, vote_account) = create_test_account(); + let credits_through_epoch_1: Vec = vec![2, 1]; + let credits_through_epoch_2: Vec = vec![2, 1, 3]; + + let clock_epoch_3 = &Clock { + epoch: 3, + ..Clock::default() + }; // unsigned request let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)]; @@ -2236,6 +2292,7 @@ mod tests { ), &signers, None, + None, ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); @@ -2253,17 +2310,24 @@ mod tests { ), &signers, None, + Some(&Clock::default()), ); assert_eq!(res, Err(InstructionError::InsufficientFunds)); - // non rent exempt withdraw, before feature activation + // non rent exempt withdraw, before 7txXZZD6 feature activation + // without 0 credit epoch, before ALBk3EWd feature activation { - let (vote_pubkey, vote_account) = create_test_account(); - let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let lamports = vote_account.borrow().lamports(); + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); let rent_sysvar = Rent::default(); let minimum_balance = rent_sysvar - .minimum_balance(vote_account.borrow().data().len()) + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) .max(1); assert!(minimum_balance <= lamports); let signers: HashSet = get_signers(keyed_accounts); @@ -2277,18 +2341,217 @@ mod tests { ), &signers, None, + None, ); assert_eq!(res, Ok(())); } - // non rent exempt withdraw, after feature activation + // non rent exempt withdraw, before 7txXZZD6 feature activation + // with 0 credit epoch, before ALBk3EWd feature activation { - let (vote_pubkey, vote_account) = create_test_account(); - let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let lamports = vote_account.borrow().lamports(); + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); let rent_sysvar = Rent::default(); let minimum_balance = rent_sysvar - .minimum_balance(vote_account.borrow().data().len()) + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + None, + None, + ); + assert_eq!(res, Ok(())); + } + + // non rent exempt withdraw, before 7txXZZD6 feature activation + // without 0 credit epoch, after ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + None, + Some(clock_epoch_3), + ); + assert_eq!(res, Ok(())); + } + + // non rent exempt withdraw, before 7txXZZD6 feature activation + // with 0 credit epoch, after ALBk3EWd activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + None, + Some(clock_epoch_3), + ); + assert_eq!(res, Ok(())); + } + + // non rent exempt withdraw, after 7txXZZD6 feature activation + // with 0 credit epoch, before ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + Some(&rent_sysvar), + None, + ); + assert_eq!(res, Err(InstructionError::InsufficientFunds)); + } + + // non rent exempt withdraw, after 7txXZZD6 feature activation + // without 0 credit epoch, before ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + Some(&rent_sysvar), + None, + ); + assert_eq!(res, Err(InstructionError::InsufficientFunds)); + } + + // non rent exempt withdraw, after 7txXZZD6 feature activation + // with 0 credit epoch, after ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + Some(&rent_sysvar), + Some(clock_epoch_3), + ); + assert_eq!(res, Err(InstructionError::InsufficientFunds)); + } + + // non rent exempt withdraw, after 7txXZZD6 feature activation + // without 0 credit epoch, after ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) .max(1); assert!(minimum_balance <= lamports); let signers: HashSet = get_signers(keyed_accounts); @@ -2302,11 +2565,12 @@ mod tests { ), &signers, Some(&rent_sysvar), + Some(clock_epoch_3), ); assert_eq!(res, Err(InstructionError::InsufficientFunds)); } - // partial valid withdraw, after feature activation + // partial valid withdraw, after 7txXZZD6 feature activation { let to_account = RefCell::new(AccountSharedData::default()); let (vote_pubkey, vote_account) = create_test_account(); @@ -2325,6 +2589,7 @@ mod tests { &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), &signers, Some(&rent_sysvar), + Some(&Clock::default()), ); assert_eq!(res, Ok(())); assert_eq!( @@ -2334,12 +2599,45 @@ mod tests { assert_eq!(to_account.borrow().lamports(), withdraw_lamports); } - // full withdraw, before/after activation + // full withdraw, before/after 7txXZZD6 feature activation + // with/without 0 credit epoch, before ALBk3EWd feature activation + { + let rent_sysvar = Rent::default(); + for rent_sysvar in [None, Some(&rent_sysvar)] { + for credits in [&credits_through_epoch_1, &credits_through_epoch_2] { + let to_account = RefCell::new(AccountSharedData::default()); + let (vote_pubkey, vote_account) = + create_test_account_with_epoch_credits(credits); + let lamports = vote_account.borrow().lamports(); + let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports, + &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), + &signers, + rent_sysvar, + None, + ); + assert_eq!(res, Ok(())); + assert_eq!(vote_account.borrow().lamports(), 0); + assert_eq!(to_account.borrow().lamports(), lamports); + let post_state: VoteStateVersions = vote_account.borrow().state().unwrap(); + // State has been deinitialized since balance is zero + assert!(post_state.is_uninitialized()); + } + } + } + + // full withdraw, before/after 7txXZZD6 feature activation + // with 0 credit epoch, after ALBk3EWd feature activation { let rent_sysvar = Rent::default(); for rent_sysvar in [None, Some(&rent_sysvar)] { let to_account = RefCell::new(AccountSharedData::default()); - let (vote_pubkey, vote_account) = create_test_account(); + // let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); let lamports = vote_account.borrow().lamports(); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; let signers: HashSet = get_signers(keyed_accounts); @@ -2349,6 +2647,7 @@ mod tests { &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), &signers, rent_sysvar, + Some(clock_epoch_3), ); assert_eq!(res, Ok(())); assert_eq!(vote_account.borrow().lamports(), 0); @@ -2359,6 +2658,35 @@ mod tests { } } + // full withdraw, before/after 7txXZZD6 feature activation + // without 0 credit epoch, after ALBk3EWd feature activation + { + let rent_sysvar = Rent::default(); + for rent_sysvar in [None, Some(&rent_sysvar)] { + let to_account = RefCell::new(AccountSharedData::default()); + // let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let lamports = vote_account.borrow().lamports(); + let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports, + &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), + &signers, + rent_sysvar, + Some(clock_epoch_3), + ); + assert_eq!(res, Err(InstructionError::ActiveVoteAccountClose)); + assert_eq!(vote_account.borrow().lamports(), lamports); + assert_eq!(to_account.borrow().lamports(), 0); + let post_state: VoteStateVersions = vote_account.borrow().state().unwrap(); + // State is still initialized + assert!(!post_state.is_uninitialized()); + } + } + // authorize authorized_withdrawer let authorized_withdrawer_pubkey = solana_sdk::pubkey::new_rand(); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; @@ -2388,6 +2716,7 @@ mod tests { withdrawer_keyed_account, &signers, None, + None, ); assert_eq!(res, Ok(())); assert_eq!(vote_account.borrow().lamports(), 0); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 8ea429f587b96b..552086151c705d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -214,7 +214,7 @@ impl RentDebits { } type BankStatusCache = StatusCache>; -#[frozen_abi(digest = "FPLuTUU5MjwsijzDubxY6BvBEkWULhYNUyY6Puqejb4g")] +#[frozen_abi(digest = "6XkxpmzmKZguLZMS1KmU7N2dAcv8MmNhyobJCwRLkTdi")] pub type BankSlotDelta = SlotDelta>; // Eager rent collection repeats in cyclic manner. diff --git a/sdk/program/src/instruction.rs b/sdk/program/src/instruction.rs index e1fbb1825a845a..587ecee5ffcb5c 100644 --- a/sdk/program/src/instruction.rs +++ b/sdk/program/src/instruction.rs @@ -252,6 +252,10 @@ pub enum InstructionError { /// Accounts data budget exceeded #[error("Requested account data allocation exceeded the accounts data budget")] AccountsDataBudgetExceeded, + + /// Active vote account close + #[error("Cannot close vote account unless it stopped voting at least one full epoch ago")] + ActiveVoteAccountClose, // Note: For any new error added here an equivalent ProgramError and its // conversions must also be added } diff --git a/sdk/program/src/program_error.rs b/sdk/program/src/program_error.rs index c47638dddb29b9..0f8e02dab1cf82 100644 --- a/sdk/program/src/program_error.rs +++ b/sdk/program/src/program_error.rs @@ -51,6 +51,8 @@ pub enum ProgramError { IllegalOwner, #[error("Requested account data allocation exceeded the accounts data budget")] AccountsDataBudgetExceeded, + #[error("Cannot close vote account unless it stopped voting at least one full epoch ago")] + ActiveVoteAccountClose, } pub trait PrintProgramError { @@ -90,6 +92,7 @@ impl PrintProgramError for ProgramError { Self::UnsupportedSysvar => msg!("Error: UnsupportedSysvar"), Self::IllegalOwner => msg!("Error: IllegalOwner"), Self::AccountsDataBudgetExceeded => msg!("Error: AccountsDataBudgetExceeded"), + Self::ActiveVoteAccountClose => msg!("Error: ActiveVoteAccountClose"), } } } @@ -121,6 +124,7 @@ pub const ACCOUNT_NOT_RENT_EXEMPT: u64 = to_builtin!(16); pub const UNSUPPORTED_SYSVAR: u64 = to_builtin!(17); pub const ILLEGAL_OWNER: u64 = to_builtin!(18); pub const ACCOUNTS_DATA_BUDGET_EXCEEDED: u64 = to_builtin!(19); +pub const ACTIVE_VOTE_ACCOUNT_CLOSE: u64 = to_builtin!(20); // Warning: Any new program errors added here must also be: // - Added to the below conversions // - Added as an equivilent to InstructionError @@ -148,6 +152,7 @@ impl From for u64 { ProgramError::UnsupportedSysvar => UNSUPPORTED_SYSVAR, ProgramError::IllegalOwner => ILLEGAL_OWNER, ProgramError::AccountsDataBudgetExceeded => ACCOUNTS_DATA_BUDGET_EXCEEDED, + ProgramError::ActiveVoteAccountClose => ACTIVE_VOTE_ACCOUNT_CLOSE, ProgramError::Custom(error) => { if error == 0 { CUSTOM_ZERO @@ -181,6 +186,7 @@ impl From for ProgramError { UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, ILLEGAL_OWNER => Self::IllegalOwner, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, + ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose, _ => Self::Custom(error as u32), } } @@ -210,6 +216,7 @@ impl TryFrom for ProgramError { Self::Error::UnsupportedSysvar => Ok(Self::UnsupportedSysvar), Self::Error::IllegalOwner => Ok(Self::IllegalOwner), Self::Error::AccountsDataBudgetExceeded => Ok(Self::AccountsDataBudgetExceeded), + Self::Error::ActiveVoteAccountClose => Ok(Self::ActiveVoteAccountClose), _ => Err(error), } } @@ -241,6 +248,7 @@ where UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, ILLEGAL_OWNER => Self::IllegalOwner, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, + ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose, _ => { // A valid custom error has no bits set in the upper 32 if error >> BUILTIN_BIT_SHIFT == 0 { diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index b8a954f7967721..002a32487e49a1 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -307,6 +307,10 @@ pub mod spl_associated_token_account_v1_0_4 { solana_sdk::declare_id!("FaTa4SpiaSNH44PGC4z8bnGVTkSRYaWvrBs3KTu8XQQq"); } +pub mod reject_vote_account_close_unless_zero_credit_epoch { + solana_sdk::declare_id!("ALBk3EWdeAg2WAGf6GPDUf1nynyNqCdEVmgouG7rpuCj"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -378,6 +382,7 @@ lazy_static! { (update_syscall_base_costs::id(), "Update syscall base costs"), (vote_withdraw_authority_may_change_authorized_voter::id(), "vote account withdraw authority may change the authorized voter #22521"), (spl_associated_token_account_v1_0_4::id(), "SPL Associated Token Account Program release version 1.0.4, tied to token 3.3.0 #22648"), + (reject_vote_account_close_unless_zero_credit_epoch::id(), "fail vote account withdraw to 0 unless account earned 0 credits in last completed epoch"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/storage-proto/proto/transaction_by_addr.proto b/storage-proto/proto/transaction_by_addr.proto index ee88455e66c65f..c12cdd06173ce2 100644 --- a/storage-proto/proto/transaction_by_addr.proto +++ b/storage-proto/proto/transaction_by_addr.proto @@ -113,6 +113,7 @@ enum InstructionErrorType { UNSUPPORTED_SYSVAR = 48; ILLEGAL_OWNER = 49; ACCOUNTS_DATA_BUDGET_EXCEEDED = 50; + ACTIVE_VOTE_ACCOUNT_CLOSE = 51; } message UnixTimestamp { diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 02698cb6194ca6..f3ad0395d46287 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -689,6 +689,7 @@ impl TryFrom for TransactionError { 48 => InstructionError::UnsupportedSysvar, 49 => InstructionError::IllegalOwner, 50 => InstructionError::AccountsDataBudgetExceeded, + 51 => InstructionError::ActiveVoteAccountClose, _ => return Err("Invalid InstructionError"), }; @@ -979,6 +980,9 @@ impl From for tx_by_addr::TransactionError { InstructionError::AccountsDataBudgetExceeded => { tx_by_addr::InstructionErrorType::AccountsDataBudgetExceeded } + InstructionError::ActiveVoteAccountClose => { + tx_by_addr::InstructionErrorType::ActiveVoteAccountClose + } } as i32, custom: match instruction_error { InstructionError::Custom(custom) => {